diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 000000000..132103bbb --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,18 @@ +FROM mcr.microsoft.com/vscode/devcontainers/dotnet:0.201.7-5.0 + +# Install Mono for running tests +RUN sudo apt install gnupg ca-certificates && \ + sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF && \ + echo "deb https://download.mono-project.com/repo/ubuntu stable-focal main" | sudo tee /etc/apt/sources.list.d/mono-official-stable.list && \ + sudo apt update && \ + sudo apt install -y mono-complete && \ +# Install .NET Core 3.1 for running tests + sudo apt-get install wget && \ + wget https://packages.microsoft.com/config/ubuntu/20.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb && \ + sudo dpkg -i packages-microsoft-prod.deb && \ + sudo apt-get update && \ + sudo apt-get install -y apt-transport-https && \ + sudo apt-get update && \ + sudo apt-get install -y dotnet-sdk-3.1 + +# Built with ❤ by [Pipeline Foundation](https://pipeline.foundation) \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..e5f1586f9 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,27 @@ +{ + "name": "RestSharp Codespace", + "settings": { + "workbench.colorTheme": "Default Dark+", + "terminal.integrated.defaultProfile.linux": "pwsh" + }, + "extensions": [ + "eamodio.gitlens", + "ms-dotnettools.csharp", + "VisualStudioExptTeam.vscodeintellicode", + "ms-vscode.powershell", + "cschleiden.vscode-github-actions", + "redhat.vscode-yaml", + "bierner.markdown-preview-github-styles", + "ban.spellright", + "jmrog.vscode-nuget-package-manager", + "coenraads.bracket-pair-colorizer", + "vscode-icons-team.vscode-icons", + "editorconfig.editorconfig" + ], + "postCreateCommand": "dotnet restore RestSharp.sln && dotnet build RestSharp.sln --configuration Release --no-restore && dotnet test RestSharp.sln --configuration Release --no-build", + "build": { + "dockerfile": "Dockerfile" + } +} + +// Built with ❤ by [Pipeline Foundation](https://pipeline.foundation) \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..6b125b4c5 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,82 @@ + +[*] + +# ReSharper properties +resharper_align_multiline_binary_expressions_chain = false +resharper_blank_lines_around_single_line_auto_property = 0 +resharper_blank_lines_around_single_line_field = 0 +resharper_blank_lines_before_multiline_statements = 1 +resharper_csharp_blank_lines_around_single_line_invocable = 1 +resharper_csharp_empty_block_style = together +resharper_csharp_int_align_comments = true +resharper_csharp_keep_blank_lines_in_code = 1 +resharper_csharp_keep_blank_lines_in_declarations = 1 +resharper_csharp_wrap_after_declaration_lpar = true +resharper_csharp_wrap_after_invocation_lpar = true +resharper_csharp_wrap_arguments_style = chop_if_long +resharper_csharp_wrap_before_declaration_rpar = true +resharper_csharp_wrap_before_invocation_rpar = true +resharper_csharp_wrap_parameters_style = chop_if_long +resharper_int_align_assignments = true +resharper_int_align_fields = true +resharper_int_align_nested_ternary = true +resharper_int_align_parameters = true +resharper_int_align_properties = true +resharper_int_align_property_patterns = true +resharper_int_align_switch_expressions = true +resharper_int_align_switch_sections = true +resharper_int_align_variables = true +resharper_place_accessorholder_attribute_on_same_line = false +resharper_place_expr_method_on_single_line = if_owner_is_single_line +resharper_place_expr_property_on_single_line = true +resharper_place_simple_initializer_on_single_line = false +resharper_show_autodetect_configure_formatting_tip = false +resharper_use_indent_from_vs = false +resharper_wrap_array_initializer_style = chop_always +resharper_wrap_before_arrow_with_expressions = true +resharper_wrap_chained_binary_expressions = chop_if_long +resharper_wrap_object_and_collection_initializer_style = chop_always +resharper_invert_if_highlighting = none + +# Microsoft .NET properties +csharp_using_directive_placement = outside_namespace +csharp_new_line_before_members_in_object_initializers = false +csharp_new_line_before_open_brace = none +csharp_preferred_modifier_order = public, private, protected, internal, new, abstract, virtual, sealed, override, static, readonly, extern, unsafe, volatile, async:suggestion +csharp_style_var_elsewhere = true:suggestion +csharp_style_var_for_built_in_types = true:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion +dotnet_naming_style.lower_camel_case_style.capitalization = camel_case +dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:none +dotnet_style_parentheses_in_other_binary_operators = never_if_unnecessary:none +dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:none +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion +dotnet_style_qualification_for_event = false:suggestion +dotnet_style_qualification_for_field = false:suggestion +dotnet_style_qualification_for_method = false:suggestion +dotnet_style_qualification_for_property = false:suggestion +dotnet_style_require_accessibility_modifiers = never:suggestion +object_creation_when_type_evident = target_typed +object_creation_when_type_not_evident = target_typed + +# ReSharper inspection severities +resharper_arrange_redundant_parentheses_highlighting = hint +resharper_arrange_this_qualifier_highlighting = hint +resharper_arrange_type_member_modifiers_highlighting = hint +resharper_arrange_type_modifiers_highlighting = hint +resharper_built_in_type_reference_style_for_member_access_highlighting = hint +resharper_built_in_type_reference_style_highlighting = hint +resharper_redundant_base_qualifier_highlighting = warning +resharper_suggest_var_or_type_built_in_types_highlighting = hint +resharper_suggest_var_or_type_elsewhere_highlighting = hint +resharper_suggest_var_or_type_simple_types_highlighting = hint +resharper_web_config_module_not_resolved_highlighting = warning +resharper_web_config_type_not_resolved_highlighting = warning +resharper_web_config_wrong_module_highlighting = warning +resharper_redundant_using_directive_highlighting = error + +[*.{appxmanifest,asax,ascx,aspx,axaml,build,cg,cginc,compute,cs,cshtml,dtd,fs,fsi,fsscript,fsx,hlsl,hlsli,hlslinc,master,ml,mli,nuspec,paml,razor,resw,resx,shader,skin,usf,ush,vb,xaml,xamlx,xoml,xsd}] +indent_style = space +indent_size = 4 +tab_width = 4 diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..129655002 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +github: restsharp diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..78f38d41b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,41 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: 'bug' +assignees: '' + +--- + +**DO NOT USE ISSUES FOR QUESTIONS** + +Note: New issues raised, where it is clear the submitter has not read the issue template, +are likely to be closed with `invalid` label. Please understand that this is not meant to be rude, +but to keep the issue list clean and useful for everyone. If one opening the issue decides to ignore the issue template, +the maintainers might also decide to ignore the issue. + +**Remove** all the text above the next line when submitting your issue. + +**Describe the bug** +A clear and concise description of what the bug is. +Hint: use a tool like https://requestbin.com to compare working and non-working requests. + +**To Reproduce** +Steps to reproduce the behavior, preferably using a code snippet. +Post the non-working request here as well if you made it work using Postman, Swagger, or any other client. + +**Expected behavior** +A clear and concise description of what you expected to happen. +Post the working request here as well if you made it work using Postman, Swagger, or any other client. +You can use https://requestbin.com/r to create a public request bin and share the link in the issue. + +**Stack trace** +Copy the full stack trace here if you get an exception. + +**Desktop (please complete the following information):** + - OS: [e.g. macOS] + - .NET version [e.g. .NET 6] + - Version [e.g. 110.2.0] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..45aa98fc8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Get help + url: https://restsharp.dev/support/#get-help + about: Read about asking questions and reporting issues \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..30dd38d77 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,22 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: 'feature-request' +assignees: '' + +--- + +**DO NOT USE ISSUES FOR QUESTIONS** + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..ebb4939b2 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,18 @@ +## Description + + + +## Purpose +This pull request is a: + +- [ ] Bugfix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) + + +## Checklist + + + +- [ ] I have added tests that prove my fix is effective or that my feature works +- [ ] I have added necessary documentation (if appropriate) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 000000000..4a54b40d6 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,113 @@ +# Copilot Instructions for RestSharp + +This document provides instructions for GitHub Copilot when working with the RestSharp repository. + +## Project Overview + +RestSharp is a lightweight HTTP API client library for .NET. It wraps `HttpClient` and provides: +- Default parameters of any kind, not just headers +- Multiple ways to add request parameters (query, URL segment, header, cookie, body) +- Built-in serialization/deserialization for JSON, XML, and CSV +- Rich support for authentication + +## Repository Structure + +- `src/RestSharp/` - Main library +- `src/RestSharp.Serializers.*/` - Serializer extensions (NewtonsoftJson, Xml, CsvHelper) +- `gen/SourceGenerator/` - Incremental source generators +- `test/RestSharp.Tests/` - Unit tests +- `test/RestSharp.Tests.Integrated/` - Integration tests (WireMock) +- `test/RestSharp.Tests.Serializers.*/` - Serializer-specific tests +- `benchmarks/RestSharp.Benchmarks/` - Performance tests + +## Build and Test Commands + +```bash +# Build solution +dotnet build RestSharp.sln -c Debug + +# Run all tests +dotnet test RestSharp.sln -c Debug + +# Run tests for specific TFM +dotnet test test/RestSharp.Tests/RestSharp.Tests.csproj -f net8.0 + +# Run single test +dotnet test test/RestSharp.Tests/RestSharp.Tests.csproj --filter "FullyQualifiedName=Namespace.Class.Method" -f net8.0 + +# Pack for release +dotnet pack src/RestSharp/RestSharp.csproj -c Release -o nuget -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg +``` + +## Multi-Targeting + +The library targets multiple frameworks: +- `netstandard2.0`, `net471`, `net48` - Legacy support +- `net8.0`, `net9.0`, `net10.0` - Modern .NET + +Tests target: `net48` (Windows only), `net8.0`, `net9.0`, `net10.0` + +When adding features: +- Use conditional compilation for TFM-specific APIs: `#if NET` or `#if NET8_0_OR_GREATER` +- Ensure compilation succeeds for all TFMs +- Add polyfills or conditional code for missing APIs on older TFMs + +## Code Style and Conventions + +- Language version: C# preview (latest features allowed) +- Nullable reference types: Enabled in `/src`, disabled in `/test` +- All `/src` files must include the Apache 2.0 license header +- Test files do not require the license header +- Follow `.editorconfig` for formatting rules +- Use partial classes for large types (link with ``) +- Assemblies are strong-named via `RestSharp.snk` + +### License Header for Source Files + +```csharp +// 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. +``` + +## Testing Guidelines + +- Frameworks: xUnit + FluentAssertions + AutoFixture +- Use WireMockServer for HTTP scenarios (avoid live endpoints) +- Use descriptive assertions: `result.Should().Be(expected)` +- Guard TFM-specific tests with `#if NET8_0_OR_GREATER` +- Avoid flaky tests: don't depend on timing, locale, or network conditions + +Test projects have global usings configured for `Xunit`, `FluentAssertions`, and `AutoFixture`. + +## Source Generators + +Custom generators in `gen/SourceGenerator/`: +- `[GenerateImmutable]` - Creates read-only wrappers +- `[GenerateClone]` - Creates static factory clone methods +- `[Exclude]` - Excludes properties from immutable generation + +Generator target: `netstandard2.0` (required for all compiler versions) + +## Dependencies + +Package versions are centrally managed in `Directory.Packages.props`. Do not pin versions in individual projects unless justified by TFM constraints. + +## PR Checklist + +Before submitting: +- Builds cleanly across all targeted TFMs +- Tests added/updated and passing +- No new analyzer warnings in `/src` +- License header in new `/src` files +- Public API changes documented and tested diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..fa38860ea --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,15 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + # default location of `.github/workflows` + directory: "/" + schedule: + interval: "weekly" + + - package-ecosystem: "nuget" + # location of package manifests + directory: "/" + schedule: + interval: "daily" + +# Built with ❤ by [Pipeline Foundation](https://pipeline.foundation) \ No newline at end of file diff --git a/.github/ranger.yml b/.github/ranger.yml new file mode 100644 index 000000000..f2e098b37 --- /dev/null +++ b/.github/ranger.yml @@ -0,0 +1,49 @@ +default: + close: + delay: "3 days" + comment: "⚠️ This issue has been marked $LABEL and will be closed in $DELAY" + remind: + action: label + delay: 7d + message: "A reminder to provide the necessary information or feedback, waiting for $DELAY days" + labels: + - stale + +labels: + awaiting-feedback: + action: remind + awaiting-info: + action: remind + duplicate: + action: close + delay: 15s + comment: "Duplicate issue created! Closing in $DELAY..." + invalid: close + stale: + action: close + delay: 14 days + comment: false + 'merge when passing': merge + approved: + action: merge + 'new contributor': + action: comment + delay: 5s + message: "Thanks for making your first contribution! :slightly_smiling_face:" + +merges: + - action: delete_branch + +comments: + - action: label + pattern: /duplicate of/i + labels: + - duplicate + - action: delete_comment + pattern: "+1" + +commits: + - action: label + pattern: /merge when passing/i + labels: + - merge when passing \ No newline at end of file diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 000000000..979e69cfc --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,20 @@ +# Number of days of inactivity before an issue becomes stale +daysUntilStale: 30 +# Number of days of inactivity before a stale issue is closed +daysUntilClose: 7 +# Issues with these labels will never be considered stale +exemptLabels: + - pinned + - security + - keep +# Label to use when marking an issue as stale +staleLabel: wontfix +# Comment to post when marking an issue as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. +# Comment to post when closing a stale issue. Set to `false` to disable +closeComment: > + This issue has been closed because it has not had recent activity. + Thank you for your contributions. diff --git a/.github/workflows/build-dev.yml b/.github/workflows/build-dev.yml new file mode 100644 index 000000000..65ca2899f --- /dev/null +++ b/.github/workflows/build-dev.yml @@ -0,0 +1,48 @@ +name: Build and deploy + +on: + push: + paths-ignore: + - 'docs/**' + - 'yarn.lock' + - 'package.json' + - '**/*.md' + branches: + - dev + tags: + - '*' + +permissions: + contents: read + id-token: write + +jobs: + nuget: + runs-on: ubuntu-latest + + permissions: + id-token: write + + steps: + - + name: Checkout + uses: actions/checkout@v6 + - + name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: '10.0.x' + - + name: Unshallow + run: git fetch --prune --unshallow + - + name: NuGet login + uses: NuGet/login@v1 + id: nuget-login + with: + user: ${{ secrets.NUGET_USER }} + - + name: Create and push NuGet package + run: | + dotnet pack -c Release -o nuget -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg + dotnet nuget push **/*.nupkg --api-key ${{ steps.nuget-login.outputs.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate \ No newline at end of file diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml new file mode 100644 index 000000000..92807d7e3 --- /dev/null +++ b/.github/workflows/pull-request.yml @@ -0,0 +1,77 @@ +name: Build and test PRs + +on: + pull_request: + paths-ignore: + - docs/** + +permissions: + contents: read + checks: write + +jobs: + event_file: + name: "Event File" + runs-on: ubuntu-latest + steps: + - name: Upload + uses: actions/upload-artifact@v6 + with: + name: Event File + path: ${{ github.event_path }} + + test-windows: + runs-on: windows-latest + strategy: + matrix: + dotnet: ['net48', 'net8.0', 'net9.0', 'net10.0'] + + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: | + 8.0.x + 9.0.x + 10.0.x + - name: Run tests + run: dotnet test -c Debug -f ${{ matrix.dotnet }} + - name: Upload Test Results + if: always() + uses: actions/upload-artifact@v6 + with: + name: Test Results Windows ${{ matrix.dotnet }} + path: | + test-results/**/*.xml + test-results/**/*.trx + test-results/**/*.json + + test-linux: + runs-on: ubuntu-latest + strategy: + matrix: + dotnet: [ 'net8.0', 'net9.0', 'net10.0' ] + + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: | + 8.0.x + 9.0.x + 10.0.x + - name: Run tests + run: dotnet test -f ${{ matrix.dotnet }} + - name: Upload Test Results + if: always() + uses: actions/upload-artifact@v6 + with: + name: Test Results Ubuntu ${{ matrix.dotnet }} + path: | + test-results/**/*.xml + test-results/**/*.trx + test-results/**/*.json diff --git a/.github/workflows/test-results.yml b/.github/workflows/test-results.yml new file mode 100644 index 000000000..205421fe9 --- /dev/null +++ b/.github/workflows/test-results.yml @@ -0,0 +1,37 @@ +name: Test Results + +on: + workflow_run: + workflows: ["Build and test PRs"] + types: + - completed +permissions: {} + +jobs: + test-results: + name: Test Results + runs-on: ubuntu-latest + if: github.event.workflow_run.conclusion != 'skipped' + + permissions: + checks: write + pull-requests: write + actions: read + + steps: + - + name: Download and Extract Artifacts + uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 + with: + run_id: ${{ github.event.workflow_run.id }} + path: artifacts + - + name: Publish Test Results + uses: EnricoMi/publish-unit-test-result-action@v2 + with: + commit: ${{ github.event.workflow_run.head_sha }} + event_file: artifacts/Event File/event.json + event_name: ${{ github.event.workflow_run.event }} + files: | + artifacts/**/*.xml + artifacts/**/*.trx \ No newline at end of file diff --git a/.gitignore b/.gitignore index 225eac2cf..2b2ca2d47 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ -packages +packages/ +nuget.config + #ignore thumbnails created by windows Thumbs.db @@ -37,3 +39,21 @@ Download/ *.pidb *.userprefs restsharp-computed.nuspec +Backup/ +*.htm +*.orig +DownloadSigned/ +RestSharp.IntegrationTests/config.json +/.idea/.idea.RestSharp +/.vs/RestSharp/v15/sqlite3 +/RestSharp.UWP/project.lock.json +*.ncrunchproject +*.ncrunchsolution +/.vs/RestSharp/v15/Server/sqlite3 +/.vs/ +/node_modules/ +/out/ +/docs/.vuepress/dist/ +.vscode/ +.temp/ +*.trx \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 000000000..9b0e31c26 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,6 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +/material_theme_project_new.xml diff --git a/.idea/RestSharp.iml b/.idea/RestSharp.iml new file mode 100644 index 000000000..0c8867d7e --- /dev/null +++ b/.idea/RestSharp.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 000000000..1fe75c51a --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 000000000..94a25f7f4 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.nuget/NuGet.Config b/.nuget/NuGet.Config deleted file mode 100644 index 67f8ea046..000000000 --- a/.nuget/NuGet.Config +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.nuget/NuGet.exe b/.nuget/NuGet.exe deleted file mode 100644 index 79482f60d..000000000 Binary files a/.nuget/NuGet.exe and /dev/null differ diff --git a/.nuget/NuGet.targets b/.nuget/NuGet.targets deleted file mode 100644 index 3ae18ce9f..000000000 --- a/.nuget/NuGet.targets +++ /dev/null @@ -1,71 +0,0 @@ - - - - $(MSBuildProjectDirectory)\..\ - - - $([System.IO.Path]::Combine($(SolutionDir), ".nuget")) - $([System.IO.Path]::Combine($(ProjectDir), "packages.config")) - $([System.IO.Path]::Combine($(SolutionDir), "packages")) - - - $(SolutionDir).nuget - packages.config - $(SolutionDir)packages - - - $(NuGetToolsPath)\nuget.exe - "$(NuGetExePath)" - mono --runtime=v4.0.30319 $(NuGetExePath) - - $(TargetDir.Trim('\\')) - - - "" - - - false - - - false - - - $(NuGetCommand) install "$(PackagesConfig)" -source $(PackageSources) -o "$(PackagesDir)" - $(NuGetCommand) pack "$(ProjectPath)" -p Configuration=$(Configuration) -o "$(PackageOutputDir)" -symbols - - - - RestorePackages; - $(BuildDependsOn); - - - - - $(BuildDependsOn); - BuildPackage; - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.nuget/RestSharp.Build.dll b/.nuget/RestSharp.Build.dll deleted file mode 100644 index f09dcd41d..000000000 Binary files a/.nuget/RestSharp.Build.dll and /dev/null differ diff --git a/BEST_PRACTICES.md b/BEST_PRACTICES.md new file mode 100644 index 000000000..bc0a58802 --- /dev/null +++ b/BEST_PRACTICES.md @@ -0,0 +1,214 @@ +# RestSharp – Best Practices + +This document captures practical guidance for developing, testing, reviewing, and releasing changes in the RestSharp repository. It consolidates conventions used across the solution and provides checklists and examples to reduce regressions and ensure consistency across target frameworks. + + +## 1) Daily Development Workflow + +- Prefer small, focused pull requests with clear descriptions and test coverage. +- Build and test locally before pushing. Validate all relevant target frameworks (TFMs) for your changes. +- Keep commits tidy; prefer meaningful commit messages over large "fix" commits. +- Follow repository code style and file organization rules (see sections below). +- Use the latest C# language features (C# 14). + +Suggested loop: +- dotnet build +- dotnet test + +## 2) Multi-Targeting Guidance + +The core library targets netstandard2.0, net471, net48, net8.0, net9.0, and net10.0. Tests target net48 (Windows only), net8.0, net9.0, and net10.0. + +- Use conditional compilation for TFM-specific APIs or behaviors: + - #if NET + - #if NET for platform attributes (e.g., [UnsupportedOSPlatform("browser")]) +- When adding features, ensure compilation succeeds for all TFMs. If an API is missing on older TFMs, add polyfills or conditional code, or guard with feature detection. +- Be mindful of System.Text.Json: it is a package dependency on older TFMs but built-in on modern TFMs. +- Validate tests for each TFM impacted by your changes. If a test only applies to modern .NET, guard it with #if NET8_0_OR_GREATER. + + +## 3) Build and Configuration + +- Shared build logic lives in props/Common.props, src/Directory.Build.props, and test/Directory.Build.props. Do not duplicate MSBuild settings in individual projects unless necessary. +- Language version is preview; prefer modern C# features but ensure cross-TFM compatibility. +- Nullable reference types: + - Enabled in /src (Nullable=enable). Treat nullable warnings as design feedback. + - Disabled in /test (Nullable=disable) for test authoring ergonomics. +- Assemblies are strong-named via RestSharp.snk. Do not remove signing. +- Package versions are centrally managed in Directory.Packages.props; do not pin versions locally unless justified by TFM constraints. + + +## 4) Source Generators + +Custom incremental generators live in gen/SourceGenerator and are referenced as analyzers by src/RestSharp. + +- Use [GenerateImmutable] to produce immutable wrappers of mutable classes. + - Exclude properties with [Exclude] when not needed in the immutable type. + - Generated files are emitted to obj///generated/SourceGenerator/. +- Use [GenerateClone(BaseType=..., Name=...)] to create static factory methods that upcast/copy base properties into derived types. +- When editing generators: + - Keep the generator targets netstandard2.0. + - Ensure EmitCompilerGeneratedFiles is enabled when debugging locally. + - Validate output by building and inspecting generated files under obj/... +- Keep generator helpers cohesive (Extensions.cs) and prefer small, composable utilities. + + +## 5) Testing Strategy + +- Frameworks and libraries: xUnit + FluentAssertions + AutoFixture. +- Organization: + - Unit tests in test/RestSharp.Tests. + - Integration tests (HTTP/WireMock) in test/RestSharp.Tests.Integrated. + - Serializer-specific tests in dedicated projects. + - Shared helpers in test/RestSharp.Tests.Shared. +- Best practices: + - Co-locate tests with feature areas; use partial classes to split large suites. + - Prefer WireMockServer over live endpoints for HTTP scenarios. + - Avoid flaky tests: don’t depend on timing, locale, or network conditions. + - Use descriptive assertions, e.g., result.Should().Be(expected). + - Scope tests by TFM when API availability differs. +- Useful commands: + - dotnet test RestSharp.sln -c Debug + - dotnet test test/RestSharp.Tests/RestSharp.Tests.csproj -f net8.0 + - dotnet test test/RestSharp.Tests/RestSharp.Tests.csproj --filter "FullyQualifiedName=Namespace.Class.Method" -f net8.0 +- Test results are written to test-results//.trx. + + +## 6) Code Coverage + +- Use coverlet.collector for data-collector-based coverage. +- Example command (Cobertura output): + - dotnet test test/RestSharp.Tests/RestSharp.Tests.csproj -f net10.0 --collect:"XPlat Code Coverage" -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=cobertura +- Coverage artifacts are placed alongside test results in test-results. + + +## 7) Dependency Injection and Extensions + +- DI extensions live in src/RestSharp.Extensions.DependencyInjection. +- When updating DI helpers: + - Keep APIs minimal and idiomatic for Microsoft.Extensions.DependencyInjection. + - Maintain backward compatibility where feasible; add overloads rather than breaking changes. + - Add focused tests under test/RestSharp.Tests.DependencyInjection. + + +## 8) Serialization Extensions + +- JSON (Newtonsoft.Json), XML, and CSV serializers are separate packages within src/RestSharp.Serializers.*. +- Keep serializer-specific behavior isolated. Avoid coupling core library to serializers. +- Each serializer project has its own tests; ensure behavior parity across TFMs. + + +## 9) Performance and Benchmarks + +- Use benchmarks/RestSharp.Benchmarks for perf investigations. +- Before merging performance-related changes: + - Validate allocations and throughput with BenchmarkDotNet where practical. + - Check for regression across TFMs if the code path is shared. + + +## 10) Versioning and Packaging + +- Versioning is handled by MinVer via Git tags and history. Ensure CI uses full history (unshallow fetch). +- Do not hardcode versions; rely on MinVer for assembly and file versions (CustomVersion target aligns versions post-MinVer). +- Packaging notes: + - dotnet pack src/RestSharp/RestSharp.csproj -c Release -o nuget -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg + - Symbol packages must be .snupkg; SourceLink is enabled for debugging. + - NuGet metadata is defined in src/Directory.Build.props; keep it accurate. + + +## 11) Continuous Integration + +- PR workflow executes matrix tests on Windows (net48, net8.0, net9.0, net10.0) and Linux (net8.0, net9.0, net10.0). +- Build/Deploy workflow packages and pushes to NuGet on dev branch and tags using OIDC-based auth. +- To simulate CI locally: + - dotnet test -c Debug -f net8.0 + - Optionally also run -f net9.0 and -f net10.0. On Windows, include net48. +- Uploading artifacts and publishing test results are CI-responsibilities; keep local output organized in test-results for quick inspection. + + +## 12) Code Organization and Style + +- File organization: + - Use partial classes to split large types by responsibility (e.g., RestClient.*, PropertyCache.*). Link related files via in csproj when appropriate. +- License header: + - All source files under /src must include the standard repository license header. + - Test files do not require the header. +- EditorConfig and rules: + - Follow .editorconfig for formatting and analyzer rules. + - In /src, suppress XML doc warnings via NoWarn=1591; in tests suppress xUnit1033 and CS8002 as configured. +- Nullable: + - Treat nullable annotations and warnings as design signals; prefer explicit nullability and defensive checks at public boundaries. +- Public API surface: + - Avoid breaking changes. If unavoidable, document in docs and changelog, and add clear migration notes. + + +## 13) PR Readiness Checklist + +Use this quick checklist before requesting review: +- Builds cleanly across all targeted TFMs for affected projects. +- Unit/integration tests added or updated; all pass locally for relevant TFMs. +- No analyzer warnings introduced in src (beyond allowed suppressions). +- License header present in new /src files. +- Source generator changes validated (if applicable) by inspecting obj/.../generated output. +- Public API changes reviewed, documented, and tested. +- Central package versions unchanged unless intentionally updated with justification. +- Commit messages and PR description explain the why and the how. + + +## 14) Troubleshooting Guide + +- Tests fail only on a specific TFM + - Run with -f to isolate. Look for conditional compilation and API availability differences. +- Generator output missing + - Ensure EmitCompilerGeneratedFiles is true in the project under test; clean and rebuild; inspect obj/.../generated. +- net48 failures on non-Windows + - Expected. Use net8.0 or higher on Linux/macOS. Run net48 only on Windows. +- MinVer version incorrect + - Ensure CI or local clone has full git history (git fetch --prune --unshallow). +- Platform-specific failures + - Use [UnsupportedOSPlatform] and conditional compilation to guard APIs not available on Browser, etc. + + +## 15) Useful Commands (Quick Reference) + +- Build solution (Release): + - dotnet build RestSharp.sln -c Release +- Run tests for a single TFM: + - dotnet test test/RestSharp.Tests/RestSharp.Tests.csproj -f net8.0 +- Run a single test by fully-qualified name: + - dotnet test test/RestSharp.Tests/RestSharp.Tests.csproj --filter "FullyQualifiedName=RestSharp.Tests.ObjectParserTests.ShouldUseRequestProperty" -f net8.0 +- Pack locally with symbols: + - dotnet pack src/RestSharp/RestSharp.csproj -c Release -o nuget -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg +- View generated source files after build: + - 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 + + +## 16) Documentation and References + +- Main docs: https://restsharp.dev +- Repository: https://github.com/restsharp/RestSharp +- NuGet Packages: + - RestSharp + - RestSharp.Serializers.NewtonsoftJson + - RestSharp.Serializers.Xml + - RestSharp.Serializers.CsvHelper +- License: Apache-2.0 + +## 17) Working with Issues + +- Use issues for bugs and feature requests only. For questions and support, use: + - StackOverflow with tag `restsharp` +- When creating an issue: + - Provide clear, concise description and reproduction steps. + - Include relevant code snippets or links to repro projects. + - Use appropriate labels and templates. +- When addressing an issue: + - Avoid changing the default behavior unless absolutely necessary. + - Avoid breaking the existing API. + - Leave the existing tests to catch possible regressions. + - Add new tests for the fixed cases. + +Keep this document up-to-date when build properties, TFMs, CI workflows, or repository conventions change. \ No newline at end of file diff --git a/CONTRIBUTING.markdown b/CONTRIBUTING.md similarity index 52% rename from CONTRIBUTING.markdown rename to CONTRIBUTING.md index 3a037712a..8a308e70e 100644 --- a/CONTRIBUTING.markdown +++ b/CONTRIBUTING.md @@ -2,15 +2,18 @@ Follow these guidelines, in no particular order, to improve your chances of havi ### Before you do anything else - * Before reporting an issue or creating a pull request, discuss it in the Google Group http://groups.google.com/group/restsharp + * DO: Read about [getting help](https://restsharp.dev/support/#get-help) in the docs. + * DO: Follow the guidelines below when contributing. + * DO: Discuss bigger change in the issue before implementing it. + * DO NOT: Use issues to ask questions about using the library. ### Once a contribution is ready to be submitted * Make each pull request atomic and exclusive; don't send pull requests for a laundry list of changes. * Even better, commit in small manageable chunks. - * Tabs, not spaces. Bracket style doesn't matter. Do not reformat code you didn't touch. - * Changes to XmlDeserializer or JsonDeserializer must be accompanied by a unit test covering the change. + * Spaces, not tabs. Bracket style doesn't matter. Do not reformat code you didn't touch. + * Changes should be accompanied by unit tests to show what was broken and how your patch fixes it. * No regions except for license header - * Code must build for .NET 3.5 Client Profile, Silverlight 4 and Windows Phone 7 + * Code must build for .NET 4.5.2 and .NET Standard 2.0 * If you didn't write the code you must provide a reference to where you obtained it and preferably the license. - * Use autocrlf=true `git config --global core.autocrlf true` http://help.github.com/dealing-with-lineendings/ \ No newline at end of file + * Use autocrlf=true `git config --global core.autocrlf true` http://help.github.com/dealing-with-lineendings/ diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 000000000..fa169fa0d --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,53 @@ + + + true + + + 10.0.0 + 10.0.0 + + + 9.0.10 + + + 8.0.21 + + + 10.0.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt index 9aa0a58ae..6ff179708 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,3 +1,6 @@ +Copyright (c) .NET Foundation and Contributors +All Rights Reserved + Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ diff --git a/README.markdown b/README.markdown deleted file mode 100644 index f13bd7256..000000000 --- a/README.markdown +++ /dev/null @@ -1,70 +0,0 @@ -# RestSharp - Simple .NET REST Client - -### [Official Site/Blog][1] - [@RestSharp][2] -### Please use the [Google Group][3] for feature requests and troubleshooting usage. -### License: Apache License 2.0 - -### Features - -* Supports .NET 3.5+, Silverlight 4, Windows Phone 7, Mono, MonoTouch, Mono for Android, Compact Framework 3.5 -* Easy installation using [NuGet](http://nuget.org/packages/RestSharp) for most .NET flavors -* Automatic XML and JSON deserialization -* Supports custom serialization and deserialization via ISerializer and IDeserializer -* Fuzzy element name matching ('product_id' in XML/JSON will match C# property named 'ProductId') -* Automatic detection of type of content returned -* GET, POST, PUT, HEAD, OPTIONS, DELETE supported -* Other non-standard HTTP methods also supported -* oAuth 1, oAuth 2, Basic, NTLM and Parameter-based Authenticators included -* Supports custom authentication schemes via IAuthenticator -* Multi-part form/file uploads -* T4 Helper to generate C# classes from an XML document - -```csharp -var client = new RestClient("http://example.com"); -// client.Authenticator = new HttpBasicAuthenticator(username, password); - -var request = new RestRequest("resource/{id}", Method.POST); -request.AddParameter("name", "value"); // adds to POST or URL querystring based on Method -request.AddUrlSegment("id", "123"); // replaces matching token in request.Resource - -// add parameters for all properties on an object -request.AddObject(object); - -// or just whitelisted properties -request.AddObject(object, "PersonId", "Name", ...); - -// easily add HTTP Headers -request.AddHeader("header", "value"); - -// add files to upload (works with compatible verbs) -request.AddFile(path); - -// execute the request -IRestResponse response = client.Execute(request); -var content = response.Content; // raw content as string - -// or automatically deserialize result -// return content type is sniffed but can be explicitly set via RestClient.AddHandler(); -IRestResponse response2 = client.Execute(request); -var name = response2.Data.Name; - -// or download and save file to disk -client.DownloadData(request).SaveAs(path); - -// easy async support -client.ExecuteAsync(request, response => { - Console.WriteLine(response.Content); -}); - -// async with deserialization -var asyncHandle = client.ExecuteAsync(request, response => { - Console.WriteLine(response.Data.Name); -}); - -// abort the request on demand -asyncHandle.Abort(); -``` - - [1]: http://restsharp.org - [2]: http://twitter.com/RestSharp - [3]: http://groups.google.com/group/RestSharp diff --git a/README.md b/README.md new file mode 100644 index 000000000..e128a9ad9 --- /dev/null +++ b/README.md @@ -0,0 +1,87 @@ +# RestSharp - Simple .NET REST Client + +![](https://img.shields.io/nuget/dt/RestSharp) [![](https://img.shields.io/nuget/v/RestSharp)](https://www.nuget.org/packages/RestSharp) [![](https://img.shields.io/nuget/vpre/RestSharp)](https://www.nuget.org/packages/RestSharp#versions-body-tab) + +RestSharp is a lightweight HTTP API client library. It's a wrapper around `HttpClient`, not a full-fledged client on +its own. + +What RestSharp adds to `HttpClient`: +- Default parameters of any kind, not just headers +- Add a parameter of any kind to requests, like query, URL segment, header, cookie, or body +- Multiple ways to add a request body, including JSON, XML, URL-encoded form data, multipart form data with and + without files +- Built-in serialization and deserilization of JSON, XML, and CSV, as well as the ability to add custom serializers +- Rich support for authentication + +## Compatibility note + +RestSharp 107 was a major release that brings a lot of changes. We've removed a lot of legacy code and added new +features. Finally, RestSharp has moved to `HttpClient`. We also deprecated the following: +- SimpleJson in favour of `System.Text.Json.JsonSerialzer` +- `IRestRequest`, and `IRestResponse` in favour of implementing classes +- Everything `Http` and `IHttp` as those are just wrappers +- Client configuration moved to `RestClientOptions` to make the client thread-safe +- `IRestClient` interface surface substantially reduced + +Most of the client and some of the request options are now in `RestClientOptions`. + +Check [v107+ docs](https://restsharp.dev/v107) for more information. + +## Packages + +| Package | What it's for | +|----------------------------------------|--------------------------------------------------------------------------------------| +| `RestSharp` | The core library, including `System.Text.Json` serializer and basical XML serializer | +| `RestSharp.Serializers.NewtonsoftJson` | Use `Newtonsoft.Json` as a JSON serializer | +| `RestSharp.Serializers.Xml` | Use custom RestSharp XML serializer for XML | +| `RestSharp.Serializers.CsvHelper` | Use `CsvHelper` as a CSV serializer | + +## Code of Conduct + +This project has adopted the code of conduct defined by the Contributor Covenant to clarify expected behavior in our community. +For more information see the [.NET Foundation Code of Conduct](https://dotnetfoundation.org/code-of-conduct). + +**Users violated the aforementioned code of conduct will be blocked.** + +## Support + +RestSharp is an open-source project with a single maintainer. Do not expect your issue to be resolved unless it concerns a large group of RestSharp users. +The best way to resolve your issue is to fix it yourself. Fork the repository and submit a pull request. +You can also motivate the maintainer by sponsoring this project. + +### Contribute + +Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on the process for reporting issues and submitting pull requests. + +### Get help + +Read the docs: [Official Site][1] + +Ask a question on StackOverflow with the tag `restsharp`. + +Join RestSharp Discord server: [![Discord](https://img.shields.io/discord/1224723555053207612?label=Discord)](https://discord.gg/NdpzHZ2qep) + +Find RestSharp on Twitter: [@RestSharp][2] + +## Community + +### .NET Foundation + +This project is a part of the [.NET Foundation](https://dotnetfoundation.org). + +### Code Contributors + +This project exists thanks to all the people who contribute. +[](https://github.com/restsharp/RestSharp/graphs/contributors) + +### Financial Contributors + +Become a financial contributor and help us sustain our community. [Contribute](https://github.com/sponsors/restsharp) + +## License + +[Apache License 2.0](https://github.com/restsharp/RestSharp/blob/dev/LICENSE.txt) + + [1]: https://restsharp.dev + [2]: https://twitter.com/RestSharp + [3]: https://github.com/restsharp/RestSharp/issues diff --git a/RestSharp.Build/NuSpecUpdateTask.cs b/RestSharp.Build/NuSpecUpdateTask.cs deleted file mode 100644 index 808f7f26a..000000000 --- a/RestSharp.Build/NuSpecUpdateTask.cs +++ /dev/null @@ -1,122 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Microsoft.Build.Utilities; -using System.Reflection; -using System.Xml.Linq; -using System.IO; - -namespace RestSharp.Build -{ - public class NuSpecUpdateTask : Task - { - private Assembly _assembly; - - public string Id { get; private set; } - - public string Authors { get; private set; } - - public string Description { get; private set; } - - public string Version { get; private set; } - - public string SpecFile { get; set; } - - public string SourceAssemblyFile { get; set; } - - public NuSpecUpdateTask() - : this(null) - { - } - - public NuSpecUpdateTask(Assembly assembly) - { - this._assembly = assembly; - } - - public override bool Execute() - { - if (string.IsNullOrEmpty(this.SpecFile)) return false; - - var path = Path.GetFullPath(this.SourceAssemblyFile); - this._assembly = this._assembly ?? Assembly.LoadFile(path); - - var name = this._assembly.GetName(); - - this.Id = name.Name; - this.Authors = this.GetAuthors(this._assembly); - this.Description = this.GetDescription(this._assembly); - this.Version = this.GetVersion(this._assembly); - - this.GenerateComputedSpecFile(); - - return true; - } - - private void GenerateComputedSpecFile() - { - var doc = XDocument.Load(this.SpecFile); - - var metaNode = doc.Descendants("metadata").First(); - - this.ReplaceToken(metaNode, "id", this.Id); - this.ReplaceToken(metaNode, "authors", this.Authors); - this.ReplaceToken(metaNode, "owners", this.Authors); - this.ReplaceToken(metaNode, "description", this.Description); - this.ReplaceToken(metaNode, "version", this.Version); - - doc.Save(this.SpecFile.Replace(".nuspec", "-computed.nuspec")); - } - - private void ReplaceToken(XElement metaNode, XName name, string value) - { - var node = metaNode.Element(name); - var token = string.Format("${0}$", name.ToString().TrimEnd('s')); - - if (name.ToString().Equals("owners")) - { - token = "$author$"; - } - - if (node.Value.Equals(token, StringComparison.OrdinalIgnoreCase)) - { - node.SetValue(value); - } - } - - private string GetDescription(Assembly asm) - { - return this.GetAttribute(asm).Description; - } - - private string GetAuthors(Assembly asm) - { - return this.GetAttribute(asm).Company; - } - - private string GetVersion(Assembly asm) - { - var version = asm.GetName().Version.ToString(); - var attr = this.GetAttribute(asm); - - if (attr != null) - { - version = attr.InformationalVersion; - } - - return version; - } - - private TAttr GetAttribute(Assembly asm) where TAttr : Attribute - { - var attrs = asm.GetCustomAttributes(typeof(TAttr), false); - if (attrs.Length > 0) - { - return attrs[0] as TAttr; - } - - return null; - } - } -} diff --git a/RestSharp.Build/Properties/AssemblyInfo.cs b/RestSharp.Build/Properties/AssemblyInfo.cs deleted file mode 100644 index 27c7602cf..000000000 --- a/RestSharp.Build/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("RestSharp.Build")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("RestSharp.Build")] -[assembly: AssemblyCopyright("Copyright © 2013")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("1cf9b3e7-67bb-4dca-9094-5f8c94ef9587")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/RestSharp.Build/RestSharp.Build.csproj b/RestSharp.Build/RestSharp.Build.csproj deleted file mode 100644 index 1892a1640..000000000 --- a/RestSharp.Build/RestSharp.Build.csproj +++ /dev/null @@ -1,56 +0,0 @@ - - - - Debug - AnyCPU - 8.0.30703 - 2.0 - {CCC30138-3D68-44D8-AF1A-D22F769EE8DC} - Library - Properties - RestSharp.Build - RestSharp.Build - v3.5 - 512 - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - true - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/RestSharp.Compact.sln b/RestSharp.Compact.sln deleted file mode 100644 index 39ae77eb7..000000000 --- a/RestSharp.Compact.sln +++ /dev/null @@ -1,20 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 10.00 -# Visual Studio 2008 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestSharp.Compact", "RestSharp\RestSharp.Compact.csproj", "{A29E330B-F854-4287-BEB2-C4CBE9D9C637}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {A29E330B-F854-4287-BEB2-C4CBE9D9C637}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A29E330B-F854-4287-BEB2-C4CBE9D9C637}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A29E330B-F854-4287-BEB2-C4CBE9D9C637}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A29E330B-F854-4287-BEB2-C4CBE9D9C637}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection -EndGlobal diff --git a/RestSharp.IntegrationTests/App.config b/RestSharp.IntegrationTests/App.config deleted file mode 100644 index 050a07624..000000000 --- a/RestSharp.IntegrationTests/App.config +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/RestSharp.IntegrationTests/AsyncRequestBodyTests.cs b/RestSharp.IntegrationTests/AsyncRequestBodyTests.cs deleted file mode 100644 index d0d654e4a..000000000 --- a/RestSharp.IntegrationTests/AsyncRequestBodyTests.cs +++ /dev/null @@ -1,197 +0,0 @@ -using System.IO; -using System.Net; -using System.Threading; -using RestSharp.IntegrationTests.Helpers; -using Xunit; - -namespace RestSharp.IntegrationTests -{ - public class AsyncRequestBodyTests - { - private const string BASE_URL = "http://localhost:8080/"; - - [Fact] - public void Can_Not_Be_Added_To_GET_Request() - { - const Method httpMethod = Method.GET; - using (SimpleServer.Create(BASE_URL, Handlers.Generic())) - { - var client = new RestClient(BASE_URL); - var request = new RestRequest(RequestBodyCapturer.RESOURCE, httpMethod); - - const string contentType = "text/plain"; - const string bodyData = "abc123 foo bar baz BING!"; - request.AddParameter(contentType, bodyData, ParameterType.RequestBody); - - var resetEvent = new ManualResetEvent(false); - client.ExecuteAsync(request, response => resetEvent.Set()); - resetEvent.WaitOne(); - - AssertHasNoRequestBody(); - } - } - - [Fact] - public void Can_Be_Added_To_POST_Request() - { - const Method httpMethod = Method.POST; - using (SimpleServer.Create(BASE_URL, Handlers.Generic())) - { - var client = new RestClient(BASE_URL); - var request = new RestRequest(RequestBodyCapturer.RESOURCE, httpMethod); - - const string contentType = "text/plain"; - const string bodyData = "abc123 foo bar baz BING!"; - request.AddParameter(contentType, bodyData, ParameterType.RequestBody); - - var resetEvent = new ManualResetEvent(false); - client.ExecuteAsync(request, response => resetEvent.Set()); - resetEvent.WaitOne(); - - AssertHasRequestBody(contentType, bodyData); - } - } - - [Fact] - public void Can_Be_Added_To_PUT_Request() - { - const Method httpMethod = Method.PUT; - using (SimpleServer.Create(BASE_URL, Handlers.Generic())) - { - var client = new RestClient(BASE_URL); - var request = new RestRequest(RequestBodyCapturer.RESOURCE, httpMethod); - - const string contentType = "text/plain"; - const string bodyData = "abc123 foo bar baz BING!"; - request.AddParameter(contentType, bodyData, ParameterType.RequestBody); - - var resetEvent = new ManualResetEvent(false); - client.ExecuteAsync(request, response => resetEvent.Set()); - resetEvent.WaitOne(); - - AssertHasRequestBody(contentType, bodyData); - } - } - - [Fact] - public void Can_Be_Added_To_DELETE_Request() - { - const Method httpMethod = Method.DELETE; - using (SimpleServer.Create(BASE_URL, Handlers.Generic())) - { - var client = new RestClient(BASE_URL); - var request = new RestRequest(RequestBodyCapturer.RESOURCE, httpMethod); - - const string contentType = "text/plain"; - const string bodyData = "abc123 foo bar baz BING!"; - request.AddParameter(contentType, bodyData, ParameterType.RequestBody); - - var resetEvent = new ManualResetEvent(false); - client.ExecuteAsync(request, response => resetEvent.Set()); - resetEvent.WaitOne(); - - AssertHasRequestBody(contentType, bodyData); - } - } - - [Fact] - public void Can_Not_Be_Added_To_HEAD_Request() - { - const Method httpMethod = Method.HEAD; - using (SimpleServer.Create(BASE_URL, Handlers.Generic())) - { - var client = new RestClient(BASE_URL); - var request = new RestRequest(RequestBodyCapturer.RESOURCE, httpMethod); - - const string contentType = "text/plain"; - const string bodyData = "abc123 foo bar baz BING!"; - request.AddParameter(contentType, bodyData, ParameterType.RequestBody); - - var resetEvent = new ManualResetEvent(false); - client.ExecuteAsync(request, response => resetEvent.Set()); - resetEvent.WaitOne(); - - AssertHasNoRequestBody(); - } - } - - [Fact] - public void Can_Be_Added_To_OPTIONS_Request() - { - const Method httpMethod = Method.OPTIONS; - using (SimpleServer.Create(BASE_URL, Handlers.Generic())) - { - var client = new RestClient(BASE_URL); - var request = new RestRequest(RequestBodyCapturer.RESOURCE, httpMethod); - - const string contentType = "text/plain"; - const string bodyData = "abc123 foo bar baz BING!"; - request.AddParameter(contentType, bodyData, ParameterType.RequestBody); - - var resetEvent = new ManualResetEvent(false); - client.ExecuteAsync(request, response => resetEvent.Set()); - resetEvent.WaitOne(); - - AssertHasRequestBody(contentType, bodyData); - } - } - - [Fact] - public void Can_Be_Added_To_PATCH_Request() - { - const Method httpMethod = Method.PATCH; - using (SimpleServer.Create(BASE_URL, Handlers.Generic())) - { - var client = new RestClient(BASE_URL); - var request = new RestRequest(RequestBodyCapturer.RESOURCE, httpMethod); - - const string contentType = "text/plain"; - const string bodyData = "abc123 foo bar baz BING!"; - request.AddParameter(contentType, bodyData, ParameterType.RequestBody); - - var resetEvent = new ManualResetEvent(false); - client.ExecuteAsync(request, response => resetEvent.Set()); - resetEvent.WaitOne(); - - AssertHasRequestBody(contentType, bodyData); - } - } - - private static void AssertHasNoRequestBody() - { - Assert.Null(RequestBodyCapturer.CapturedContentType); - Assert.Equal(false, RequestBodyCapturer.CapturedHasEntityBody); - Assert.Equal(string.Empty, RequestBodyCapturer.CapturedEntityBody); - } - - private static void AssertHasRequestBody(string contentType, string bodyData) - { - Assert.Equal(contentType, RequestBodyCapturer.CapturedContentType); - Assert.Equal(true, RequestBodyCapturer.CapturedHasEntityBody); - Assert.Equal(bodyData, RequestBodyCapturer.CapturedEntityBody); - } - - private class RequestBodyCapturer - { - public const string RESOURCE = "Capture"; - - public static string CapturedContentType { get; set; } - public static bool CapturedHasEntityBody { get; set; } - public static string CapturedEntityBody { get; set; } - - public static void Capture(HttpListenerContext context) - { - var request = context.Request; - CapturedContentType = request.ContentType; - CapturedHasEntityBody = request.HasEntityBody; - CapturedEntityBody = StreamToString(request.InputStream); - } - - private static string StreamToString(Stream stream) - { - var streamReader = new StreamReader(stream); - return streamReader.ReadToEnd(); - } - } - } -} diff --git a/RestSharp.IntegrationTests/AsyncTests.cs b/RestSharp.IntegrationTests/AsyncTests.cs deleted file mode 100644 index 3feed6347..000000000 --- a/RestSharp.IntegrationTests/AsyncTests.cs +++ /dev/null @@ -1,259 +0,0 @@ -using System.Linq; -using System.Threading; -using System.Net; -using RestSharp.IntegrationTests.Helpers; -using Xunit; - -namespace RestSharp.IntegrationTests -{ - public class AsyncTests - { - [Fact] - public void Can_Perform_GET_Async() - { - const string baseUrl = "http://localhost:8080/"; - const string val = "Basic async test"; - var resetEvent = new ManualResetEvent(false); - using (SimpleServer.Create(baseUrl, Handlers.EchoValue(val))) - { - var client = new RestClient(baseUrl); - var request = new RestRequest(""); - - client.ExecuteAsync(request, (response, asyncHandle) => - { - Assert.NotNull(response.Content); - Assert.Equal(val, response.Content); - resetEvent.Set(); - }); - resetEvent.WaitOne(); - } - } - - [Fact] - public void Can_Perform_GET_Async_Without_Async_Handle() - { - const string baseUrl = "http://localhost:8080/"; - const string val = "Basic async test"; - var resetEvent = new ManualResetEvent(false); - using (SimpleServer.Create(baseUrl, Handlers.EchoValue(val))) - { - var client = new RestClient(baseUrl); - var request = new RestRequest(""); - - client.ExecuteAsync(request, response => - { - Assert.NotNull(response.Content); - Assert.Equal(val, response.Content); - resetEvent.Set(); - }); - resetEvent.WaitOne(); - } - } - - [Fact] - public void Can_Perform_GET_TaskAsync() - { - const string baseUrl = "http://localhost:8080/"; - const string val = "Basic async task test"; - using (SimpleServer.Create(baseUrl, Handlers.EchoValue(val))) - { - var client = new RestClient(baseUrl); - var request = new RestRequest(""); - - var task = client.ExecuteTaskAsync(request); - task.Wait(); - - Assert.NotNull(task.Result.Content); - Assert.Equal(val, task.Result.Content); - } - } - - [Fact] - public void Can_Perform_ExecuteGetTaskAsync_With_Response_Type() - { - const string baseUrl = "http://localhost:8080/"; - using (SimpleServer.Create(baseUrl, Handlers.Generic())) - { - var client = new RestClient(baseUrl); - var request = new RestRequest("success"); - - var task = client.ExecuteTaskAsync(request); - task.Wait(); - - Assert.Equal("Works!", task.Result.Data.Message); - } - } - - [Fact] - public void Can_Perform_GetTaskAsync_With_Response_Type() - { - const string baseUrl = "http://localhost:8080/"; - using (SimpleServer.Create(baseUrl, Handlers.Generic())) - { - var client = new RestClient(baseUrl); - var request = new RestRequest("success"); - - var task = client.GetTaskAsync(request); - task.Wait(); - - Assert.Equal("Works!", task.Result.Message); - } - } - - [Fact] - public void Can_Cancel_GET_TaskAsync() - { - const string baseUrl = "http://localhost:8080/"; - const string val = "Basic async task test"; - using (SimpleServer.Create(baseUrl, Handlers.EchoValue(val))) - { - var client = new RestClient(baseUrl); - var request = new RestRequest("timeout"); - var cancellationTokenSource = new CancellationTokenSource(); - - var task = client.ExecuteTaskAsync(request, cancellationTokenSource.Token); - cancellationTokenSource.Cancel(); - - Assert.True(task.IsCanceled); - } - } - - [Fact] - public void Can_Cancel_GET_TaskAsync_With_Response_Type() - { - const string baseUrl = "http://localhost:8080/"; - const string val = "Basic async task test"; - using (SimpleServer.Create(baseUrl, Handlers.EchoValue(val))) - { - var client = new RestClient(baseUrl); - var request = new RestRequest("timeout"); - var cancellationTokenSource = new CancellationTokenSource(); - - var task = client.ExecuteTaskAsync(request, cancellationTokenSource.Token); - cancellationTokenSource.Cancel(); - - Assert.True(task.IsCanceled); - } - } - - [Fact] - public void Handles_GET_Request_Errors_TaskAsync() - { - const string baseUrl = "http://localhost:8080/"; - using (SimpleServer.Create(baseUrl, UrlToStatusCodeHandler)) - { - var client = new RestClient(baseUrl); - var request = new RestRequest("404"); - - var task = client.ExecuteTaskAsync(request); - task.Wait(); - - Assert.Equal(HttpStatusCode.NotFound, task.Result.StatusCode); - } - } - - [Fact] - public void Handles_GET_Request_Errors_TaskAsync_With_Response_Type() - { - const string baseUrl = "http://localhost:8080/"; - using (SimpleServer.Create(baseUrl, UrlToStatusCodeHandler)) - { - var client = new RestClient(baseUrl); - var request = new RestRequest("404"); - - var task = client.ExecuteTaskAsync(request); - task.Wait(); - - Assert.Null(task.Result.Data); - } - } - - [Fact] - public void Can_Timeout_GET_TaskAsync() - { - const string baseUrl = "http://localhost:8080/"; - using (SimpleServer.Create(baseUrl, Handlers.Generic())) - { - var client = new RestClient(baseUrl); - var request = new RestRequest("timeout", Method.GET).AddBody("Body_Content"); - - //Half the value of ResponseHandler.Timeout - request.Timeout = 500; - - System.AggregateException agg = Assert.Throws( - delegate - { - var task = client.ExecuteTaskAsync(request); - task.Wait(); - }); - - Assert.IsType(typeof(WebException), agg.InnerException); - Assert.Equal("The request timed-out.", agg.InnerException.Message); - } - } - - [Fact] - public void Can_Timeout_PUT_TaskAsync() - { - const string baseUrl = "http://localhost:8080/"; - using (SimpleServer.Create(baseUrl, Handlers.Generic())) - { - var client = new RestClient(baseUrl); - var request = new RestRequest("timeout", Method.PUT).AddBody("Body_Content"); - - //Half the value of ResponseHandler.Timeout - request.Timeout = 500; - - System.AggregateException agg = Assert.Throws( - delegate - { - var task = client.ExecuteTaskAsync(request); - task.Wait(); - }); - - Assert.IsType(typeof(WebException), agg.InnerException); - Assert.Equal("The request timed-out.", agg.InnerException.Message); - } - } - - void UrlToStatusCodeHandler(HttpListenerContext obj) - { - obj.Response.StatusCode = int.Parse(obj.Request.Url.Segments.Last()); - } - - public class ResponseHandler - { - void error(HttpListenerContext context) - { - context.Response.StatusCode = 400; - context.Response.Headers.Add("Content-Type", "application/xml"); - context.Response.OutputStream.WriteStringUtf8( - @" - - - Not found! - -"); - } - void success(HttpListenerContext context) - { - context.Response.OutputStream.WriteStringUtf8( - @" - - - Works! - -"); - } - void timeout(HttpListenerContext context) - { - Thread.Sleep(1000); - } - } - - public class Response - { - public string Message { get; set; } - } - } -} diff --git a/RestSharp.IntegrationTests/AuthenticationTests.cs b/RestSharp.IntegrationTests/AuthenticationTests.cs deleted file mode 100644 index 345612b34..000000000 --- a/RestSharp.IntegrationTests/AuthenticationTests.cs +++ /dev/null @@ -1,146 +0,0 @@ -using System; -using System.Diagnostics; -using System.Net; -using System.Text; -using RestSharp.Authenticators; -using RestSharp.Contrib; -using RestSharp.IntegrationTests.Helpers; -using Xunit; - -namespace RestSharp.IntegrationTests -{ - public class AuthenticationTests - { - [Fact] - public void Can_Authenticate_With_Basic_Http_Auth() - { - const string baseUrl = "http://localhost:8080/"; - using(SimpleServer.Create(baseUrl, UsernamePasswordEchoHandler)) - { - var client = new RestClient(baseUrl); - client.Authenticator = new HttpBasicAuthenticator("testuser", "testpassword"); - - var request = new RestRequest("test"); - var response = client.Execute(request); - - Assert.Equal("testuser|testpassword", response.Content); - } - } - - private static void UsernamePasswordEchoHandler(HttpListenerContext context) - { - var header = context.Request.Headers["Authorization"]; - - var parts = Encoding.ASCII.GetString(Convert.FromBase64String(header.Substring("Basic ".Length))).Split(':'); - context.Response.OutputStream.WriteStringUtf8(string.Join("|", parts)); - } - - //[Fact] - public void Can_Authenticate_With_OAuth() - { - var baseUrl = "https://api.twitter.com"; - var client = new RestClient(baseUrl); - client.Authenticator = OAuth1Authenticator.ForRequestToken( - "CONSUMER_KEY", "CONSUMER_SECRET" - ); - var request = new RestRequest("oauth/request_token"); - var response = client.Execute(request); - - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - var qs = HttpUtility.ParseQueryString(response.Content); - var oauth_token = qs["oauth_token"]; - var oauth_token_secret = qs["oauth_token_secret"]; - Assert.NotNull(oauth_token); - Assert.NotNull(oauth_token_secret); - - request = new RestRequest("oauth/authorize?oauth_token=" + oauth_token); - var url = client.BuildUri(request).ToString(); - Process.Start(url); - - var verifier = "123456"; // <-- Breakpoint here (set verifier in debugger) - request = new RestRequest("oauth/access_token"); - client.Authenticator = OAuth1Authenticator.ForAccessToken( - "P5QziWtocYmgWAhvlegxw", "jBs07SIxJ0kodeU9QtLEs1W1LRgQb9u5Lc987BA94", oauth_token, oauth_token_secret, verifier - ); - response = client.Execute(request); - - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - qs = HttpUtility.ParseQueryString(response.Content); - oauth_token = qs["oauth_token"]; - oauth_token_secret = qs["oauth_token_secret"]; - Assert.NotNull(oauth_token); - Assert.NotNull(oauth_token_secret); - - request = new RestRequest("account/verify_credentials.xml"); - client.Authenticator = OAuth1Authenticator.ForProtectedResource( - "P5QziWtocYmgWAhvlegxw", "jBs07SIxJ0kodeU9QtLEs1W1LRgQb9u5Lc987BA94", oauth_token, oauth_token_secret - ); - - response = client.Execute(request); - - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - } - - //[Fact] - //public void Can_Obtain_OAuth_Request_Token() - //{ - // var baseUrl = "http://term.ie/oauth/example"; - // var client = new RestClient(baseUrl); - // client.Authenticator = new OAuthAuthenticator(baseUrl, "key", "secret"); - // var request = new RestRequest("request_token.php"); - // var response = client.Execute(request); - - // Assert.NotNull(response); - // Assert.Equal("oauth_token=requestkey&oauth_token_secret=requestsecret", response.Content); - //} - - //[Fact] - //public void Can_Obtain_OAuth_Access_Token() - //{ - // var baseUrl = "http://term.ie/oauth/example"; - // var client = new RestClient(baseUrl); - // client.Authenticator = new OAuthAuthenticator(baseUrl, "key", "secret", "requestkey", "requestsecret"); - // var request = new RestRequest("access_token.php"); - // var response = client.Execute(request); - - // Assert.NotNull(response); - // Assert.Equal("oauth_token=accesskey&oauth_token_secret=accesssecret", response.Content); - - //} - - //[Fact] - //public void Can_Make_Authenticated_OAuth_Call_With_Parameters() - //{ - // var baseUrl = "http://term.ie/oauth/example"; - // var client = new RestClient(baseUrl); - // client.Authenticator = new OAuthAuthenticator(baseUrl, "key", "secret", "accesskey", "accesssecret"); - // var request = new RestRequest("echo_api.php"); - // request.AddParameter("foo", "bar"); - // request.AddParameter("fizz", "pop"); - // var response = client.Execute(request); - - // Assert.NotNull(response); - // Assert.Equal("fizz=pop&foo=bar", response.Content); - //} - - //[Fact] - //public void Can_Make_Authenticated_OAuth_Call() - //{ - // var baseUrl = "http://term.ie/oauth/example"; - // var client = new RestClient(baseUrl); - // client.Authenticator = new OAuthAuthenticator(baseUrl, "key", "secret", "accesskey", "accesssecret"); - // var request = new RestRequest("echo_api.php"); - // var response = client.Execute(request); - - // Assert.NotNull(response); - // Assert.Equal(string.Empty, response.Content); - - //} - - } -} diff --git a/RestSharp.IntegrationTests/CompressionTests.cs b/RestSharp.IntegrationTests/CompressionTests.cs deleted file mode 100644 index 69f63ca99..000000000 --- a/RestSharp.IntegrationTests/CompressionTests.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System; -using System.IO.Compression; -using System.Net; -using RestSharp.IntegrationTests.Helpers; -using Xunit; - -namespace RestSharp.IntegrationTests -{ - public class CompressionTests - { - [Fact] - public void Can_Handle_Gzip_Compressed_Content() - { - const string baseUrl = "http://localhost:8080/"; - using(SimpleServer.Create(baseUrl, GzipEchoValue("This is some gzipped content"))) - { - var client = new RestClient(baseUrl); - var request = new RestRequest(""); - var response = client.Execute(request); - - Assert.Equal("This is some gzipped content", response.Content); - } - } - - [Fact] - public void Can_Handle_Deflate_Compressed_Content() - { - const string baseUrl = "http://localhost:8080/"; - using(SimpleServer.Create(baseUrl, DeflateEchoValue("This is some deflated content"))) - { - var client = new RestClient(baseUrl); - var request = new RestRequest(""); - var response = client.Execute(request); - - Assert.Equal("This is some deflated content", response.Content); - } - } - - [Fact] - public void Can_Handle_Uncompressed_Content() - { - const string baseUrl = "http://localhost:8080/"; - using(SimpleServer.Create(baseUrl, Handlers.EchoValue("This is some sample content"))) - { - var client = new RestClient(baseUrl); - var request = new RestRequest(""); - var response = client.Execute(request); - - Assert.Equal("This is some sample content", response.Content); - } - } - - static Action GzipEchoValue(string value) - { - return context => - { - context.Response.Headers.Add("Content-encoding", "gzip"); - using (var gzip = new GZipStream(context.Response.OutputStream, CompressionMode.Compress, true)) - { - gzip.WriteStringUtf8(value); - } - }; - } - - static Action DeflateEchoValue(string value) - { - return context => - { - context.Response.Headers.Add("Content-encoding", "deflate"); - using(var gzip = new DeflateStream(context.Response.OutputStream, CompressionMode.Compress, true)) - { - gzip.WriteStringUtf8(value); - } - }; - } - } -} diff --git a/RestSharp.IntegrationTests/FileTests.cs b/RestSharp.IntegrationTests/FileTests.cs deleted file mode 100644 index 867ec8968..000000000 --- a/RestSharp.IntegrationTests/FileTests.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.IO; -using RestSharp.IntegrationTests.Helpers; -using Xunit; - -namespace RestSharp.IntegrationTests -{ - public class FileTests - { - [Fact] - public void Handles_Binary_File_Download() - { - const string baseUrl = "http://localhost:8080/"; - using (SimpleServer.Create(baseUrl, Handlers.FileHandler)) - { - var client = new RestClient(baseUrl); - var request = new RestRequest("Assets/Koala.jpg"); - var response = client.DownloadData(request); - - var expected = File.ReadAllBytes(Environment.CurrentDirectory + "\\Assets\\Koala.jpg"); - Assert.Equal(expected, response); - } - } - - [Fact] - public void Writes_Response_To_Stream() - { - const string baseUrl = "http://localhost:8080/"; - using (SimpleServer.Create(baseUrl, Handlers.FileHandler)) - { - string tempFile = Path.GetTempFileName(); - using (var writer = File.OpenWrite(tempFile)) - { - var client = new RestClient(baseUrl); - var request = new RestRequest("Assets/Koala.jpg"); - request.ResponseWriter = (responseStream) => responseStream.CopyTo(writer); - var response = client.DownloadData(request); - Assert.Null(response); - } - var fromTemp = File.ReadAllBytes(tempFile); - var expected = File.ReadAllBytes(Environment.CurrentDirectory + "\\Assets\\Koala.jpg"); - Assert.Equal(expected, fromTemp); - } - } - } -} \ No newline at end of file diff --git a/RestSharp.IntegrationTests/Helpers/Extensions.cs b/RestSharp.IntegrationTests/Helpers/Extensions.cs deleted file mode 100644 index 776567b59..000000000 --- a/RestSharp.IntegrationTests/Helpers/Extensions.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.IO; -using System.Text; - -namespace RestSharp.IntegrationTests.Helpers -{ - public static class Extensions - { - public static void WriteStringUtf8(this Stream target, string value) - { - var encoded = Encoding.UTF8.GetBytes(value); - target.Write(encoded, 0, encoded.Length); - } - } -} \ No newline at end of file diff --git a/RestSharp.IntegrationTests/Helpers/Handlers.cs b/RestSharp.IntegrationTests/Helpers/Handlers.cs deleted file mode 100644 index b7f6f2e39..000000000 --- a/RestSharp.IntegrationTests/Helpers/Handlers.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Net; -using System.Reflection; - -namespace RestSharp.IntegrationTests.Helpers -{ - public static class Handlers - { - /// - /// Echoes the request input back to the output. - /// - public static void Echo(HttpListenerContext context) - { - context.Request.InputStream.CopyTo(context.Response.OutputStream); - } - - /// - /// Echoes the given value back to the output. - /// - public static Action EchoValue(string value) - { - return ctx => ctx.Response.OutputStream.WriteStringUtf8(value); - } - - /// - /// Response to a request like this: http://localhost:8080/assets/koala.jpg - /// by streaming the file located at "assets\koala.jpg" back to the client. - /// - public static void FileHandler(HttpListenerContext context) - { - var pathToFile = Path.Combine(context.Request.Url.Segments.Select(s => s.Replace("/", "")).ToArray()); - - using(var reader = new StreamReader(pathToFile)) - reader.BaseStream.CopyTo(context.Response.OutputStream); - } - - /// - /// T should be a class that implements methods whose names match the urls being called, and take one parameter, an HttpListenerContext. - /// e.g. - /// urls exercised: "http://localhost:8080/error" and "http://localhost:8080/get_list" - /// - /// class MyHandler - /// { - /// void error(HttpListenerContext ctx) - /// { - /// // do something interesting here - /// } - /// - /// void get_list(HttpListenerContext ctx) - /// { - /// // do something interesting here - /// } - /// } - /// - public static Action Generic() where T : new() - { - return ctx => - { - var methodName = ctx.Request.Url.Segments.Last(); - var method = typeof(T).GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static); - - if(method.IsStatic) - { - method.Invoke(null, new object[] { ctx }); - } - else - { - method.Invoke(new T(), new object[] { ctx }); - } - }; - } - } -} \ No newline at end of file diff --git a/RestSharp.IntegrationTests/Helpers/SimpleServer.cs b/RestSharp.IntegrationTests/Helpers/SimpleServer.cs deleted file mode 100644 index 0087b9b0a..000000000 --- a/RestSharp.IntegrationTests/Helpers/SimpleServer.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.Net; -using System.Threading; - -namespace RestSharp.IntegrationTests.Helpers -{ - public class SimpleServer : IDisposable - { - readonly HttpListener _listener; - readonly Action _handler; - Thread _processor; - - public static SimpleServer Create(string url, Action handler, AuthenticationSchemes authenticationSchemes = AuthenticationSchemes.Anonymous) - { - var listener = new HttpListener - { - Prefixes = { url }, - AuthenticationSchemes = authenticationSchemes - }; - var server = new SimpleServer(listener, handler); - server.Start(); - return server; - } - - SimpleServer(HttpListener listener, Action handler) - { - _listener = listener; - _handler = handler; - } - - public void Start() - { - if(!_listener.IsListening) - { - _listener.Start(); - - _processor = new Thread(() => - { - var context = _listener.GetContext(); - _handler(context); - context.Response.Close(); - }) { Name = "WebServer" }; - _processor.Start(); - } - } - - public void Dispose() - { - _processor.Abort(); - _listener.Stop(); - _listener.Close(); - } - } -} \ No newline at end of file diff --git a/RestSharp.IntegrationTests/Models/LinkedINMemberProfile.cs b/RestSharp.IntegrationTests/Models/LinkedINMemberProfile.cs deleted file mode 100644 index 2446aed7f..000000000 --- a/RestSharp.IntegrationTests/Models/LinkedINMemberProfile.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace RestSharp.IntegrationTests.Models -{ - /// - /// Model for used by the LinkedIN integration tests. . - /// - public class LinkedINMemberProfile - { - public string Id { get; set; } - public string FirstName { get; set; } - public string LastName { get; set; } - } -} \ No newline at end of file diff --git a/RestSharp.IntegrationTests/MultipartFormDataTests.cs b/RestSharp.IntegrationTests/MultipartFormDataTests.cs deleted file mode 100644 index a0c062932..000000000 --- a/RestSharp.IntegrationTests/MultipartFormDataTests.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System; -using System.Net; -using RestSharp.IntegrationTests.Helpers; -using Xunit; - -namespace RestSharp.IntegrationTests -{ - public class MultipartFormDataTests - { - [Fact] - public void MultipartFormDataAsync() { - const string baseUrl = "http://localhost:8080/"; - using (SimpleServer.Create(baseUrl, EchoHandler)) { - var client = new RestClient(baseUrl); - var request = new RestRequest("/"); - request.Method = Method.POST; - request.AlwaysMultipartFormData = true; - AddParameters(request); - var restRequestAsyncHandle = client.ExecuteAsync(request, (restResponse, handle) => { - Console.WriteLine(restResponse.Content); - Assert.True(restResponse.Content == Expected); - }); - } - } - - [Fact] - public void MultipartFormData() { - //const string baseUrl = "http://localhost:8080/"; - const string baseUrl = "http://localhost:8080/"; - using (SimpleServer.Create(baseUrl, EchoHandler)) { - var client = new RestClient(baseUrl); - var request = new RestRequest("/"); - request.Method = Method.POST; - request.AlwaysMultipartFormData = true; - AddParameters(request); - var response = client.Execute(request); - Console.WriteLine(response.Content); - - Assert.True(response.Content == Expected); - } - } - - private void AddParameters(RestRequest request) { - request.AddParameter("foo", "bar"); - request.AddParameter("a name with spaces", "somedata"); - } - private const string Expected = @"-------------------------------28947758029299 -Content-Disposition: form-data; name=""foo"" - -bar --------------------------------28947758029299 -Content-Disposition: form-data; name=""a name with spaces"" - -somedata --------------------------------28947758029299-- -"; - - private void EchoHandler(HttpListenerContext obj) { - obj.Response.StatusCode = 200; - var streamReader = new System.IO.StreamReader(obj.Request.InputStream); - obj.Response.OutputStream.WriteStringUtf8(streamReader.ReadToEnd()); - } - } -} diff --git a/RestSharp.IntegrationTests/NonProtocolExceptionHandlingTests.cs b/RestSharp.IntegrationTests/NonProtocolExceptionHandlingTests.cs deleted file mode 100644 index 6a1410a8f..000000000 --- a/RestSharp.IntegrationTests/NonProtocolExceptionHandlingTests.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Xunit; -using RestSharp.IntegrationTests.Helpers; -using System.Net; -using System.Threading; - -namespace RestSharp.IntegrationTests -{ - public class NonProtocolExceptionHandlingTests - { - - /// - /// Success of this test is based largely on the behavior of your current DNS. - /// For example, if you're using OpenDNS this will test will fail; ResponseStatus will be Completed. - /// - [Fact] - public void Handles_Non_Existent_Domain() - { - var client = new RestClient("http://nonexistantdomainimguessing.org"); - var request = new RestRequest("foo"); - var response = client.Execute(request); - - Assert.Equal(ResponseStatus.Error, response.ResponseStatus); - } - - /// - /// Tests that RestSharp properly handles a non-protocol error. - /// Simulates a server timeout, then verifies that the ErrorException - /// property is correctly populated. - /// - [Fact] - public void Handles_Server_Timeout_Error() - { - const string baseUrl = "http://localhost:8080/"; - using (SimpleServer.Create(baseUrl, TimeoutHandler)) - { - var client = new RestClient(baseUrl); - var request = new RestRequest("404"); - var response = client.Execute(request); - - Assert.NotNull(response.ErrorException); - Assert.IsAssignableFrom(typeof(WebException), response.ErrorException); - Assert.Equal(response.ErrorException.Message, "The operation has timed out"); - - } - } - - [Fact] - public void Handles_Server_Timeout_Error_Async() - { - const string baseUrl = "http://localhost:8080/"; - var resetEvent = new ManualResetEvent(false); - - using (SimpleServer.Create(baseUrl, TimeoutHandler)) - { - var client = new RestClient(baseUrl); - var request = new RestRequest("404"); - client.ExecuteAsync(request, response => { - - Assert.NotNull(response.ErrorException); - Assert.IsAssignableFrom(typeof(WebException), response.ErrorException); - Assert.Equal(response.ErrorException.Message, "The operation has timed out"); - resetEvent.Set(); - }); - resetEvent.WaitOne(); - } - } - - /// - /// Tests that RestSharp properly handles a non-protocol error. - /// Simulates a server timeout, then verifies that the ErrorException - /// property is correctly populated. - /// - [Fact] - public void Handles_Server_Timeout_Error_With_Deserializer() - { - const string baseUrl = "http://localhost:8080/"; - using (SimpleServer.Create(baseUrl, TimeoutHandler)) - { - var client = new RestClient(baseUrl); - var request = new RestRequest("404"); - var response = client.Execute(request); - - Assert.Null(response.Data); - Assert.NotNull(response.ErrorException); - Assert.IsAssignableFrom(typeof(WebException), response.ErrorException); - Assert.Equal(response.ErrorException.Message, "The operation has timed out"); - - } - } - - - /// - /// Simulates a long server process that should result in a client timeout - /// - /// - public static void TimeoutHandler(HttpListenerContext context) - { - System.Threading.Thread.Sleep(101000); - } - - - } -} diff --git a/RestSharp.IntegrationTests/Properties/AssemblyInfo.cs b/RestSharp.IntegrationTests/Properties/AssemblyInfo.cs deleted file mode 100644 index 0e5aed3e7..000000000 --- a/RestSharp.IntegrationTests/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("RestSharp.IntegrationTests")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("Microsoft")] -[assembly: AssemblyProduct("RestSharp.IntegrationTests")] -[assembly: AssemblyCopyright("Copyright © Microsoft 2010")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("d1867cb5-67ee-49c2-afd5-3c9f371b9b4c")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/RestSharp.IntegrationTests/RequestBodyTests.cs b/RestSharp.IntegrationTests/RequestBodyTests.cs deleted file mode 100644 index 3b61d6327..000000000 --- a/RestSharp.IntegrationTests/RequestBodyTests.cs +++ /dev/null @@ -1,182 +0,0 @@ -using System.IO; -using System.Net; -using RestSharp.IntegrationTests.Helpers; -using Xunit; - -namespace RestSharp.IntegrationTests -{ - public class RequestBodyTests - { - private const string BASE_URL = "http://localhost:8080/"; - - [Fact] - public void Can_Not_Be_Added_To_GET_Request() - { - const Method httpMethod = Method.GET; - using (SimpleServer.Create(BASE_URL, Handlers.Generic())) - { - var client = new RestClient(BASE_URL); - var request = new RestRequest(RequestBodyCapturer.RESOURCE, httpMethod); - - const string contentType = "text/plain"; - const string bodyData = "abc123 foo bar baz BING!"; - request.AddParameter(contentType, bodyData, ParameterType.RequestBody); - - client.Execute(request); - - AssertHasNoRequestBody(); - } - } - - [Fact] - public void Can_Be_Added_To_POST_Request() - { - const Method httpMethod = Method.POST; - using (SimpleServer.Create(BASE_URL, Handlers.Generic())) - { - var client = new RestClient(BASE_URL); - var request = new RestRequest(RequestBodyCapturer.RESOURCE, httpMethod); - - const string contentType = "text/plain"; - const string bodyData = "abc123 foo bar baz BING!"; - request.AddParameter(contentType, bodyData, ParameterType.RequestBody); - - client.Execute(request); - - AssertHasRequestBody(contentType, bodyData); - } - } - - [Fact] - public void Can_Be_Added_To_PUT_Request() - { - const Method httpMethod = Method.PUT; - using (SimpleServer.Create(BASE_URL, Handlers.Generic())) - { - var client = new RestClient(BASE_URL); - var request = new RestRequest(RequestBodyCapturer.RESOURCE, httpMethod); - - const string contentType = "text/plain"; - const string bodyData = "abc123 foo bar baz BING!"; - request.AddParameter(contentType, bodyData, ParameterType.RequestBody); - - client.Execute(request); - - AssertHasRequestBody(contentType, bodyData); - } - } - - [Fact] - public void Can_Be_Added_To_DELETE_Request() - { - const Method httpMethod = Method.DELETE; - using (SimpleServer.Create(BASE_URL, Handlers.Generic())) - { - var client = new RestClient(BASE_URL); - var request = new RestRequest(RequestBodyCapturer.RESOURCE, httpMethod); - - const string contentType = "text/plain"; - const string bodyData = "abc123 foo bar baz BING!"; - request.AddParameter(contentType, bodyData, ParameterType.RequestBody); - - client.Execute(request); - - AssertHasRequestBody(contentType, bodyData); - } - } - - [Fact] - public void Can_Not_Be_Added_To_HEAD_Request() - { - const Method httpMethod = Method.HEAD; - using (SimpleServer.Create(BASE_URL, Handlers.Generic())) - { - var client = new RestClient(BASE_URL); - var request = new RestRequest(RequestBodyCapturer.RESOURCE, httpMethod); - - const string contentType = "text/plain"; - const string bodyData = "abc123 foo bar baz BING!"; - request.AddParameter(contentType, bodyData, ParameterType.RequestBody); - - client.Execute(request); - - AssertHasNoRequestBody(); - } - } - - [Fact] - public void Can_Be_Added_To_OPTIONS_Request() - { - const Method httpMethod = Method.OPTIONS; - using (SimpleServer.Create(BASE_URL, Handlers.Generic())) - { - var client = new RestClient(BASE_URL); - var request = new RestRequest(RequestBodyCapturer.RESOURCE, httpMethod); - - const string contentType = "text/plain"; - const string bodyData = "abc123 foo bar baz BING!"; - request.AddParameter(contentType, bodyData, ParameterType.RequestBody); - - client.Execute(request); - - AssertHasRequestBody(contentType, bodyData); - } - } - - [Fact] - public void Can_Be_Added_To_PATCH_Request() - { - const Method httpMethod = Method.PATCH; - using (SimpleServer.Create(BASE_URL, Handlers.Generic())) - { - var client = new RestClient(BASE_URL); - var request = new RestRequest(RequestBodyCapturer.RESOURCE, httpMethod); - - const string contentType = "text/plain"; - const string bodyData = "abc123 foo bar baz BING!"; - request.AddParameter(contentType, bodyData, ParameterType.RequestBody); - - client.Execute(request); - - AssertHasRequestBody(contentType, bodyData); - } - } - - private static void AssertHasNoRequestBody() - { - Assert.Null(RequestBodyCapturer.CapturedContentType); - Assert.Equal(false, RequestBodyCapturer.CapturedHasEntityBody); - Assert.Equal(string.Empty, RequestBodyCapturer.CapturedEntityBody); - } - - private static void AssertHasRequestBody(string contentType, string bodyData) - { - Assert.Equal(contentType, RequestBodyCapturer.CapturedContentType); - Assert.Equal(true, RequestBodyCapturer.CapturedHasEntityBody); - Assert.Equal(bodyData, RequestBodyCapturer.CapturedEntityBody); - } - - private class RequestBodyCapturer - { - public const string RESOURCE = "Capture"; - - public static string CapturedContentType { get; set; } - public static bool CapturedHasEntityBody { get; set; } - public static string CapturedEntityBody { get; set; } - - public static void Capture(HttpListenerContext context) - { - var request = context.Request; - CapturedContentType = request.ContentType; - CapturedHasEntityBody = request.HasEntityBody; - CapturedEntityBody = StreamToString(request.InputStream); - } - - private static string StreamToString(Stream stream) - { - var streamReader = new StreamReader(stream); - return streamReader.ReadToEnd(); - } - } - } -} diff --git a/RestSharp.IntegrationTests/RequestHeadTests.cs b/RestSharp.IntegrationTests/RequestHeadTests.cs deleted file mode 100644 index ce6593152..000000000 --- a/RestSharp.IntegrationTests/RequestHeadTests.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System.Collections.Specialized; -using System.Linq; -using System.Net; -using RestSharp.IntegrationTests.Helpers; -using Xunit; - -namespace RestSharp.IntegrationTests -{ - public class RequestHeadTests - { - private const string BASE_URL = "http://localhost:8080/"; - - public RequestHeadTests() - { - RequestHeadCapturer.Initialize(); - } - - [Fact] - public void Does_Not_Pass_Default_Credentials_When_Server_Does_Not_Negotiate() - { - const Method httpMethod = Method.GET; - using (SimpleServer.Create(BASE_URL, Handlers.Generic())) - { - var client = new RestClient(BASE_URL); - var request = new RestRequest(RequestHeadCapturer.RESOURCE, httpMethod) - { - UseDefaultCredentials = true - }; - - client.Execute(request); - - Assert.NotNull(RequestHeadCapturer.CapturedHeaders); - var keys = RequestHeadCapturer.CapturedHeaders.Keys.Cast().ToArray(); - Assert.False(keys.Contains("Authorization"), "Authorization header was present in HTTP request from client, even though server does not use the Negotiate scheme"); - } - } - - [Fact] - public void Passes_Default_Credentials_When_UseDefaultCredentials_Is_True() - { - const Method httpMethod = Method.GET; - using (SimpleServer.Create(BASE_URL, Handlers.Generic(), AuthenticationSchemes.Negotiate)) - { - var client = new RestClient(BASE_URL); - var request = new RestRequest(RequestHeadCapturer.RESOURCE, httpMethod) - { - UseDefaultCredentials = true - }; - - var response = client.Execute(request); - - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(RequestHeadCapturer.CapturedHeaders); - var keys = RequestHeadCapturer.CapturedHeaders.Keys.Cast().ToArray(); - Assert.True(keys.Contains("Authorization"), "Authorization header not present in HTTP request from client, even though UseDefaultCredentials = true"); - } - } - - [Fact] - public void Does_Not_Pass_Default_Credentials_When_UseDefaultCredentials_Is_False() - { - const Method httpMethod = Method.GET; - using (SimpleServer.Create(BASE_URL, Handlers.Generic(), AuthenticationSchemes.Negotiate)) - { - var client = new RestClient(BASE_URL); - var request = new RestRequest(RequestHeadCapturer.RESOURCE, httpMethod) - { - // UseDefaultCredentials is currently false by default, but to make the test more robust in case that ever - // changes, it's better to explicitly set it here. - UseDefaultCredentials = false - }; - - var response = client.Execute(request); - - Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); - Assert.Null(RequestHeadCapturer.CapturedHeaders); - } - } - - private class RequestHeadCapturer - { - public const string RESOURCE = "Capture"; - - public static NameValueCollection CapturedHeaders { get; set; } - - public static void Initialize() - { - CapturedHeaders = null; - } - - public static void Capture(HttpListenerContext context) - { - var request = context.Request; - CapturedHeaders = request.Headers; - } - } - } -} \ No newline at end of file diff --git a/RestSharp.IntegrationTests/RestSharp.IntegrationTests.csproj b/RestSharp.IntegrationTests/RestSharp.IntegrationTests.csproj deleted file mode 100644 index 46713a3a8..000000000 --- a/RestSharp.IntegrationTests/RestSharp.IntegrationTests.csproj +++ /dev/null @@ -1,106 +0,0 @@ - - - - Debug - AnyCPU - 9.0.30729 - 2.0 - {47D3EBB9-0300-4AF8-BAC5-740D51454A63} - Library - Properties - RestSharp.IntegrationTests - RestSharp.IntegrationTests - v4.0 - 512 - - - 3.5 - - - ..\ - true - - - true - full - false - bin\Debug\ - TRACE;DEBUG;FRAMEWORK, NET4 - prompt - 4 - AllRules.ruleset - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - AllRules.ruleset - - - - - - - - - 3.5 - - - 3.5 - - - - - False - ..\packages\xunit.1.9.2\lib\net20\xunit.dll - - - ..\packages\xunit.extensions.1.9.2\lib\net20\xunit.extensions.dll - - - - - - - - - - - - - - - - - - - - - - - Always - - - - - - - - - {5ff943a5-260f-4042-b4ce-c4977bad4ebb} - RestSharp.Net4 - - - - - - \ No newline at end of file diff --git a/RestSharp.IntegrationTests/StatusCodeTests.cs b/RestSharp.IntegrationTests/StatusCodeTests.cs deleted file mode 100644 index ad7e61762..000000000 --- a/RestSharp.IntegrationTests/StatusCodeTests.cs +++ /dev/null @@ -1,137 +0,0 @@ -using System.Linq; -using RestSharp.IntegrationTests.Helpers; -using Xunit; -using System.Net; - -namespace RestSharp.IntegrationTests -{ - public class StatusCodeTests - { - [Fact] - public void Handles_GET_Request_404_Error() - { - const string baseUrl = "http://localhost:8080/"; - using(SimpleServer.Create(baseUrl, UrlToStatusCodeHandler)) - { - var client = new RestClient(baseUrl); - var request = new RestRequest("404"); - var response = client.Execute(request); - - Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); - } - } - - [Fact] - public void Handles_GET_Request_404_Error_With_Body() - { - const string baseUrl = "http://localhost:8080/"; - using (SimpleServer.Create(baseUrl, Handlers.Generic())) - { - var client = new RestClient(baseUrl); - var request = new RestRequest("404WithBody"); - var response = client.Execute(request); - - Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); - } - } - - void UrlToStatusCodeHandler(HttpListenerContext obj) - { - obj.Response.StatusCode = int.Parse(obj.Request.Url.Segments.Last()); - } - - [Fact] - public void Handles_Different_Root_Element_On_Http_Error() - { - const string baseUrl = "http://localhost:8080/"; - using(SimpleServer.Create(baseUrl, Handlers.Generic())) - { - var client = new RestClient(baseUrl); - var request = new RestRequest("error"); - request.RootElement = "Success"; - request.OnBeforeDeserialization = resp => - { - if(resp.StatusCode == HttpStatusCode.BadRequest) - { - request.RootElement = "Error"; - } - }; - - var response = client.Execute(request); - - Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); - Assert.Equal("Not found!", response.Data.Message); - } - } - - [Fact] - public void Handles_Default_Root_Element_On_No_Error() - { - const string baseUrl = "http://localhost:8080/"; - using(SimpleServer.Create(baseUrl, Handlers.Generic())) - { - var client = new RestClient(baseUrl); - var request = new RestRequest("success"); - request.RootElement = "Success"; - request.OnBeforeDeserialization = resp => - { - if(resp.StatusCode == HttpStatusCode.NotFound) - { - request.RootElement = "Error"; - } - }; - - var response = client.Execute(request); - - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal("Works!", response.Data.Message); - } - } - } - - public class ResponseHandler - { - void error(HttpListenerContext context) - { - context.Response.StatusCode = 400; - context.Response.Headers.Add("Content-Type", "application/xml"); - context.Response.OutputStream.WriteStringUtf8( -@" - - - Not found! - -"); - } - - void errorwithbody(HttpListenerContext context) - { - context.Response.StatusCode = 400; - context.Response.Headers.Add("Content-Type", "application/xml"); - context.Response.OutputStream.WriteStringUtf8( -@" - - - Not found! - -"); - } - - - void success(HttpListenerContext context) - { - context.Response.OutputStream.WriteStringUtf8( -@" - - - Works! - -"); - } - } - - public class Response - { - public string Message { get; set; } - } -} \ No newline at end of file diff --git a/RestSharp.IntegrationTests/oAuth1Tests.cs b/RestSharp.IntegrationTests/oAuth1Tests.cs deleted file mode 100644 index ca734e50b..000000000 --- a/RestSharp.IntegrationTests/oAuth1Tests.cs +++ /dev/null @@ -1,293 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Xml.Serialization; -using RestSharp.Authenticators.OAuth; -using RestSharp.IntegrationTests.Models; -using Xunit; -using System.Net; -using RestSharp.Contrib; -using RestSharp.Authenticators; -using System.Diagnostics; -using System; - -namespace RestSharp.IntegrationTests -{ - public class oAuth1Tests - { - [Fact(Skip = "Provide your own consumer key/secret before running")] - public void Can_Authenticate_With_OAuth() - { - const string consumerKey = ""; - const string consumerSecret = ""; - - var baseUrl = "http://api.twitter.com"; - var client = new RestClient(baseUrl); - client.Authenticator = OAuth1Authenticator.ForRequestToken(consumerKey, consumerSecret); - var request = new RestRequest("oauth/request_token", Method.POST); - var response = client.Execute(request); - - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - var qs = HttpUtility.ParseQueryString(response.Content); - var oauth_token = qs["oauth_token"]; - var oauth_token_secret = qs["oauth_token_secret"]; - Assert.NotNull(oauth_token); - Assert.NotNull(oauth_token_secret); - - request = new RestRequest("oauth/authorize"); - request.AddParameter("oauth_token", oauth_token); - var url = client.BuildUri(request).ToString(); - Process.Start(url); - - var verifier = "123456"; // <-- Breakpoint here (set verifier in debugger) - request = new RestRequest("oauth/access_token", Method.POST); - client.Authenticator = OAuth1Authenticator.ForAccessToken( - consumerKey, consumerSecret, oauth_token, oauth_token_secret, verifier - ); - response = client.Execute(request); - - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - qs = HttpUtility.ParseQueryString(response.Content); - oauth_token = qs["oauth_token"]; - oauth_token_secret = qs["oauth_token_secret"]; - Assert.NotNull(oauth_token); - Assert.NotNull(oauth_token_secret); - - request = new RestRequest("account/verify_credentials.xml"); - client.Authenticator = OAuth1Authenticator.ForProtectedResource( - consumerKey, consumerSecret, oauth_token, oauth_token_secret - ); - - response = client.Execute(request); - - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - //request = new RestRequest("statuses/update.json", Method.POST); - //request.AddParameter("status", "Hello world! " + DateTime.Now.Ticks.ToString()); - //client.Authenticator = OAuth1Authenticator.ForProtectedResource( - // consumerKey, consumerSecret, oauth_token, oauth_token_secret - //); - - //response = client.Execute(request); - - //Assert.NotNull(response); - //Assert.Equal(HttpStatusCode.OK, response.StatusCode); - } - - #region Netflix test classes - - [XmlRoot("queue")] - private class Queue - { - [XmlElement("etag")] - public string Etag { get; set; } - - public List Items { get; set; } - } - - [XmlRoot("queue_item")] - private class QueueItem - { - [XmlElement("id")] - public string ID { get; set; } - - [XmlElement("position")] - public int Position { get; set; } - } - - #endregion - - //[Fact] - public void Can_Authenticate_Netflix_With_OAuth() - { - const string consumerKey = ""; - const string consumerSecret = ""; - - var baseUrl = "http://api.netflix.com"; - var client = new RestClient(baseUrl); - client.Authenticator = OAuth1Authenticator.ForRequestToken(consumerKey, consumerSecret); - var request = new RestRequest("oauth/request_token"); - var response = client.Execute(request); - - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - var qs = HttpUtility.ParseQueryString(response.Content); - var oauth_token = qs["oauth_token"]; - var oauth_token_secret = qs["oauth_token_secret"]; - var applicationName = qs["application_name"]; - Assert.NotNull(oauth_token); - Assert.NotNull(oauth_token_secret); - Assert.NotNull(applicationName); - - var baseSslUrl = "https://api-user.netflix.com"; - var sslClient = new RestClient(baseSslUrl); - request = new RestRequest("oauth/login"); - request.AddParameter("oauth_token", oauth_token); - request.AddParameter("oauth_consumer_key", consumerKey); - request.AddParameter("application_name", applicationName); - var url = sslClient.BuildUri(request).ToString(); - - Process.Start(url); - - request = new RestRequest("oauth/access_token"); // <-- Breakpoint here, login to netflix - client.Authenticator = OAuth1Authenticator.ForAccessToken( - consumerKey, consumerSecret, oauth_token, oauth_token_secret - ); - response = client.Execute(request); - - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - qs = HttpUtility.ParseQueryString(response.Content); - oauth_token = qs["oauth_token"]; - oauth_token_secret = qs["oauth_token_secret"]; - var user_id = qs["user_id"]; - Assert.NotNull(oauth_token); - Assert.NotNull(oauth_token_secret); - Assert.NotNull(user_id); - - client.Authenticator = OAuth1Authenticator.ForProtectedResource(consumerKey, consumerSecret, oauth_token, oauth_token_secret); - request = new RestRequest("users/{user_id}/queues/disc"); - request.AddUrlSegment("user_id", user_id); - request.AddParameter("max_results", "2"); - var queueResponse = client.Execute(request); - - Assert.NotNull(queueResponse); - Assert.Equal(HttpStatusCode.OK, queueResponse.StatusCode); - - Assert.NotNull(queueResponse.Data); - Assert.Equal(2, queueResponse.Data.Items.Count); - } - - [Fact] - public void Properly_Encodes_Parameter_Names() - { - var postData = new WebParameterCollection { { "name[first]", "Chuck" }, { "name[last]", "Testa" }}; - var sortedParams = OAuthTools.SortParametersExcludingSignature(postData); - - Assert.Equal("name%5Bfirst%5D", sortedParams[0].Name); - } - - [Fact] - public void Use_RFC_3986_Encoding_For_Auth_Signature_Base() - { - // reserved characters for 2396 and 3986 - var reserved2396Characters = new[] { ";", "/", "?", ":", "@", "&", "=", "+", "$", "," }; // http://www.ietf.org/rfc/rfc2396.txt - var additionalReserved3986Characters = new[] { "!", "*", "'", "(", ")" }; // http://www.ietf.org/rfc/rfc3986.txt - var reservedCharacterString = string.Join( string.Empty, reserved2396Characters.Union( additionalReserved3986Characters ) ); - - // act - var escapedString = OAuthTools.UrlEncodeRelaxed( reservedCharacterString ); - - // assert - Assert.Equal( "%3B%2F%3F%3A%40%26%3D%2B%24%2C%21%2A%27%28%29", escapedString ); - } - - [Fact( Skip = "Provide your own consumer key/secret before running" )] - public void Can_Authenticate_LinkedIN_With_OAuth() - { - const string consumerKey = "TODO_CONSUMER_KEY_HERE"; - const string consumerSecret = "TODO_CONSUMER_SECRET_HERE"; - - // request token - var client = new RestClient { - BaseUrl = "https://api.linkedin.com/uas/oauth", - Authenticator = OAuth1Authenticator.ForRequestToken( consumerKey, consumerSecret, "http://localhost" ) - }; - var requestTokenRequest = new RestRequest( "requestToken" ); - var requestTokenResponse = client.Execute( requestTokenRequest ); - Assert.NotNull( requestTokenResponse ); - Assert.Equal( HttpStatusCode.OK, requestTokenResponse.StatusCode ); - var requestTokenResponseParameters = HttpUtility.ParseQueryString( requestTokenResponse.Content ); - var requestToken = requestTokenResponseParameters[ "oauth_token" ]; - var requestSecret = requestTokenResponseParameters[ "oauth_token_secret" ]; - Assert.NotNull( requestToken ); - Assert.NotNull( requestSecret ); - - // redirect user - requestTokenRequest = new RestRequest( "authenticate?oauth_token=" + requestToken ); - var redirectUri = client.BuildUri( requestTokenRequest ); - Process.Start( redirectUri.ToString() ); - var requestUrl = "TODO: put browser URL here"; // replace this via the debugger with the return url from LinkedIN. Simply copy it from the opened browser - if ( !Debugger.IsAttached ) - { - Debugger.Launch(); - } - Debugger.Break(); - - // get the access token - var requestTokenQueryParameters = HttpUtility.ParseQueryString( new Uri( requestUrl ).Query ); - var requestVerifier = requestTokenQueryParameters[ "oauth_verifier" ]; - client.Authenticator = OAuth1Authenticator.ForAccessToken( consumerKey, consumerSecret, requestToken, requestSecret, requestVerifier ); - var requestAccessTokenRequest = new RestRequest( "accessToken" ); - var requestActionTokenResponse = client.Execute( requestAccessTokenRequest ); - Assert.NotNull( requestActionTokenResponse ); - Assert.Equal( HttpStatusCode.OK, requestActionTokenResponse.StatusCode ); - var requestActionTokenResponseParameters = HttpUtility.ParseQueryString( requestActionTokenResponse.Content ); - var accessToken = requestActionTokenResponseParameters[ "oauth_token" ]; - var accessSecret = requestActionTokenResponseParameters[ "oauth_token_secret" ]; - Assert.NotNull( accessToken ); - Assert.NotNull( accessSecret ); - } - - [Fact( Skip = "Provide your own consumer key/secret/accessToken/accessSecret before running. You can retrieve the access token/secret by running the LinkedIN oAuth test" )] - public void Can_Retrieve_Member_Profile_Field_Field_Selector_From_LinkedIN() - { - const string consumerKey = "TODO_CONSUMER_KEY_HERE"; - const string consumerSecret = "TODO_CONSUMER_SECRET_HERE"; - const string accessToken = "TODO_ACCES_TOKEN_HERE"; - const string accessSecret = "TODO_ACCES_SECRET_HERE"; - - // arrange - var client = new RestClient { - BaseUrl = "http://api.linkedin.com/v1", - Authenticator = OAuth1Authenticator.ForProtectedResource( consumerKey, consumerSecret, accessToken, accessSecret ) - }; - var request = new RestRequest( "people/~:(id,first-name,last-name)" ); - - // act - var response = client.Execute< LinkedINMemberProfile >( request ); - - // assert - Assert.NotNull( response ); - Assert.Equal( HttpStatusCode.OK, response.StatusCode ); - Assert.NotNull( response.Data ); - Assert.NotNull( response.Data.Id ); - Assert.NotNull( response.Data.FirstName ); - Assert.NotNull( response.Data.LastName ); - } - - [Fact( Skip = "Provide your own consumer key/secret before running" )] - public void Can_Query_Vimeo() - { - const string consumerKey = "TODO_CONSUMER_KEY_HERE"; - const string consumerSecret = "TODO_CONSUMER_SECRET_HERE"; - - // arrange - var client = new RestClient { - BaseUrl = "http://vimeo.com/api/rest/v2", - Authenticator = OAuth1Authenticator.ForRequestToken( consumerKey, consumerSecret ) - }; - var request = new RestRequest(); - request.AddParameter( "format", "json" ); - request.AddParameter( "method", "vimeo.videos.search" ); - request.AddParameter( "query", "weather" ); - request.AddParameter( "full_response", 1 ); - - // act - var response = client.Execute( request ); - - // assert - Assert.NotNull( response ); - Assert.Equal( HttpStatusCode.OK, response.StatusCode ); - Assert.NotNull( response.Content ); - Assert.False( response.Content.Contains( "\"stat\":\"fail\"" ) ); - Assert.True( response.Content.Contains( "\"stat\":\"ok\"" ) ); - } - } -} diff --git a/RestSharp.IntegrationTests/packages.config b/RestSharp.IntegrationTests/packages.config deleted file mode 100644 index 998913d1d..000000000 --- a/RestSharp.IntegrationTests/packages.config +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/RestSharp.Mono.sln b/RestSharp.Mono.sln deleted file mode 100644 index 44fe621c3..000000000 --- a/RestSharp.Mono.sln +++ /dev/null @@ -1,52 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 11.00 -# Visual Studio 2010 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestSharp", "RestSharp\RestSharp.csproj", "{2ECECFBF-5F3E-40EE-A963-72336DC7ABE2}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestSharp.Tests", "RestSharp.Tests\RestSharp.Tests.csproj", "{1464E4AC-18BB-4F23-8A0B-68196F9E1871}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "NuGet", "NuGet", "{E709A928-A45C-4622-A35C-CCD8EE44CA80}" - ProjectSection(SolutionItems) = preProject - restsharp.nuspec = restsharp.nuspec - EndProjectSection -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Debug|Mixed Platforms = Debug|Mixed Platforms - Debug|x86 = Debug|x86 - Release|Any CPU = Release|Any CPU - Release|Mixed Platforms = Release|Mixed Platforms - Release|x86 = Release|x86 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {1464E4AC-18BB-4F23-8A0B-68196F9E1871}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1464E4AC-18BB-4F23-8A0B-68196F9E1871}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1464E4AC-18BB-4F23-8A0B-68196F9E1871}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {1464E4AC-18BB-4F23-8A0B-68196F9E1871}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {1464E4AC-18BB-4F23-8A0B-68196F9E1871}.Debug|x86.ActiveCfg = Debug|Any CPU - {1464E4AC-18BB-4F23-8A0B-68196F9E1871}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1464E4AC-18BB-4F23-8A0B-68196F9E1871}.Release|Any CPU.Build.0 = Release|Any CPU - {1464E4AC-18BB-4F23-8A0B-68196F9E1871}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {1464E4AC-18BB-4F23-8A0B-68196F9E1871}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {1464E4AC-18BB-4F23-8A0B-68196F9E1871}.Release|x86.ActiveCfg = Release|Any CPU - {2ECECFBF-5F3E-40EE-A963-72336DC7ABE2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2ECECFBF-5F3E-40EE-A963-72336DC7ABE2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2ECECFBF-5F3E-40EE-A963-72336DC7ABE2}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {2ECECFBF-5F3E-40EE-A963-72336DC7ABE2}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {2ECECFBF-5F3E-40EE-A963-72336DC7ABE2}.Debug|x86.ActiveCfg = Debug|Any CPU - {2ECECFBF-5F3E-40EE-A963-72336DC7ABE2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2ECECFBF-5F3E-40EE-A963-72336DC7ABE2}.Release|Any CPU.Build.0 = Release|Any CPU - {2ECECFBF-5F3E-40EE-A963-72336DC7ABE2}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {2ECECFBF-5F3E-40EE-A963-72336DC7ABE2}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {2ECECFBF-5F3E-40EE-A963-72336DC7ABE2}.Release|x86.ActiveCfg = Release|Any CPU - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - EndGlobalSection - GlobalSection(MonoDevelopProperties) = preSolution - StartupItem = RestSharp\RestSharp.csproj - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection -EndGlobal diff --git a/RestSharp.MonoDroid/Extensions/ResponseStatusExtensions.cs b/RestSharp.MonoDroid/Extensions/ResponseStatusExtensions.cs deleted file mode 100644 index 9ad41bcca..000000000 --- a/RestSharp.MonoDroid/Extensions/ResponseStatusExtensions.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Net; - -namespace RestSharp.Extensions -{ - public static class ResponseStatusExtensions - { - /// - /// Convert a to a instance. - /// - /// The response status. - /// - /// responseStatus - public static WebException ToWebException(this ResponseStatus responseStatus) - { - switch (responseStatus) - { - case ResponseStatus.None: - return new WebException("The request could not be processed.", WebExceptionStatus.ServerProtocolViolation); - case ResponseStatus.Error: - return new WebException("An error occurred while processing the request.", WebExceptionStatus.ServerProtocolViolation); - case ResponseStatus.TimedOut: - return new WebException("The request timed-out.", WebExceptionStatus.Timeout); - case ResponseStatus.Aborted: - return new WebException("The request was aborted.", WebExceptionStatus.Timeout); - default: - throw new ArgumentOutOfRangeException("responseStatus"); - } - } - } -} diff --git a/RestSharp.MonoDroid/Properties/AssemblyInfo.cs b/RestSharp.MonoDroid/Properties/AssemblyInfo.cs deleted file mode 100644 index f840e66dd..000000000 --- a/RestSharp.MonoDroid/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("RestSharp.MonoDroid")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("A7B3BEB4-1B9B-4EDF-98BC-DF9EC82390BF")] \ No newline at end of file diff --git a/RestSharp.MonoDroid/RestSharp.MonoDroid.csproj b/RestSharp.MonoDroid/RestSharp.MonoDroid.csproj deleted file mode 100644 index 2b5db6f4e..000000000 --- a/RestSharp.MonoDroid/RestSharp.MonoDroid.csproj +++ /dev/null @@ -1,268 +0,0 @@ - - - - Debug - AnyCPU - 8.0.30703 - 2.0 - {1B662C38-984F-4B6A-89B5-AFF7FCBE43A2} - {EFBA0AD7-5A72-4C68-AF49-83D382785DCF};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - Library - RestSharp - RestSharp.MonoDroid - 512 - Off - armeabi - - - - - true - full - false - bin\Debug\ - TRACE;DEBUG;MONODROID;FRAMEWORK - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE;MONODROID;FRAMEWORK - prompt - 4 - - - - - - - - - - - Enum.cs - - - FileParameter.cs - - - Http.Async.cs - - - Http.cs - - - Http.Sync.cs - - - HttpCookie.cs - - - HttpFile.cs - - - HttpHeader.cs - - - HttpParameter.cs - - - HttpResponse.cs - - - IHttp.cs - - - IRestClient.cs - - - Parameter.cs - - - RestClient.Async.cs - - - RestClient.cs - - - RestClient.Sync.cs - - - RestClientExtensions.cs - - - RestRequest.cs - - - RestResponse.cs - - - SimpleJson.cs - - - Validation\Require.cs - - - Validation\Validate.cs - - - Serializers\ISerializer.cs - - - Serializers\JsonSerializer.cs - - - Serializers\SerializeAsAttribute.cs - - - Serializers\DotNetXmlSerializer.cs - - - Serializers\XmlSerializer.cs - - - Extensions\MiscExtensions.cs - - - Extensions\ReflectionExtensions.cs - - - Extensions\XmlExtensions.cs - - - Extensions\ResponseExtensions.cs - - - Deserializers\DeserializeAsAttribute.cs - - - Deserializers\IDeserializer.cs - - - Deserializers\JsonDeserializer.cs - - - Deserializers\XmlAttributeDeserializer.cs - - - Deserializers\XmlDeserializer.cs - - - Authenticators\HttpBasicAuthenticator.cs - - - Authenticators\IAuthenticator.cs - - - Authenticators\NtlmAuthenticator.cs - - - Authenticators\SimpleAuthenticator.cs - - - Extensions\StringExtensions.cs - - - Extensions\HttpUtility.cs - - - Extensions\HtmlEncoder.cs - - - Extensions\Helpers.cs - - - IHttpResponse.cs - - - IRestResponse.cs - - - IRestRequest.cs - - - RestResponseCookie.cs - - - IHttpFactory.cs - - - Authenticators\OAuth1Authenticator.cs - - - Authenticators\OAuth2Authenticator.cs - - - Authenticators\OAuth\HttpPostParameter.cs - - - Authenticators\OAuth\HttpPostParameterType.cs - - - Authenticators\OAuth\OAuthParameterHandling.cs - - - Authenticators\OAuth\OAuthSignatureMethod.cs - - - Authenticators\OAuth\OAuthSignatureTreatment.cs - - - Authenticators\OAuth\OAuthTools.cs - - - Authenticators\OAuth\OAuthType.cs - - - Authenticators\OAuth\OAuthWebQueryInfo.cs - - - Authenticators\OAuth\OAuthWorkflow.cs - - - Authenticators\OAuth\WebPair.cs - - - Authenticators\OAuth\WebPairCollection.cs - - - Authenticators\OAuth\WebParameter.cs - - - Authenticators\OAuth\WebParameterCollection.cs - - - Authenticators\OAuth\Extensions\CollectionExtensions.cs - - - Authenticators\OAuth\Extensions\OAuthExtensions.cs - - - Authenticators\OAuth\Extensions\StringExtensions.cs - - - Authenticators\OAuth\Extensions\TimeExtensions.cs - - - RestRequestAsyncHandle.cs - - - Deserializers\DotNetXmlDeserializer.cs - - - SharedAssemblyInfo.cs - - - - - - - \ No newline at end of file diff --git a/RestSharp.MonoDroid/RestSharp.MonoDroid.sln b/RestSharp.MonoDroid/RestSharp.MonoDroid.sln deleted file mode 100644 index c0233ad09..000000000 --- a/RestSharp.MonoDroid/RestSharp.MonoDroid.sln +++ /dev/null @@ -1,20 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 11.00 -# Visual Studio 2010 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestSharp.MonoDroid", "RestSharp.MonoDroid.csproj", "{1B662C38-984F-4B6A-89B5-AFF7FCBE43A2}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {1B662C38-984F-4B6A-89B5-AFF7FCBE43A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1B662C38-984F-4B6A-89B5-AFF7FCBE43A2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1B662C38-984F-4B6A-89B5-AFF7FCBE43A2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1B662C38-984F-4B6A-89B5-AFF7FCBE43A2}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection -EndGlobal diff --git a/RestSharp.MonoTouch/Extensions/ResponseStatusExtensions.cs b/RestSharp.MonoTouch/Extensions/ResponseStatusExtensions.cs deleted file mode 100644 index 9ad41bcca..000000000 --- a/RestSharp.MonoTouch/Extensions/ResponseStatusExtensions.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Net; - -namespace RestSharp.Extensions -{ - public static class ResponseStatusExtensions - { - /// - /// Convert a to a instance. - /// - /// The response status. - /// - /// responseStatus - public static WebException ToWebException(this ResponseStatus responseStatus) - { - switch (responseStatus) - { - case ResponseStatus.None: - return new WebException("The request could not be processed.", WebExceptionStatus.ServerProtocolViolation); - case ResponseStatus.Error: - return new WebException("An error occurred while processing the request.", WebExceptionStatus.ServerProtocolViolation); - case ResponseStatus.TimedOut: - return new WebException("The request timed-out.", WebExceptionStatus.Timeout); - case ResponseStatus.Aborted: - return new WebException("The request was aborted.", WebExceptionStatus.Timeout); - default: - throw new ArgumentOutOfRangeException("responseStatus"); - } - } - } -} diff --git a/RestSharp.MonoTouch/Properties/AssemblyInfo.cs b/RestSharp.MonoTouch/Properties/AssemblyInfo.cs deleted file mode 100644 index b9c231e13..000000000 --- a/RestSharp.MonoTouch/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("RestSharp.MonoTouch")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("0F11B2CA-D65D-4B63-92B2-E4C0B1AACB6E")] \ No newline at end of file diff --git a/RestSharp.MonoTouch/RestSharp.MonoTouch.csproj b/RestSharp.MonoTouch/RestSharp.MonoTouch.csproj deleted file mode 100644 index be8ecf814..000000000 --- a/RestSharp.MonoTouch/RestSharp.MonoTouch.csproj +++ /dev/null @@ -1,309 +0,0 @@ - - - - Debug - iPhoneSimulator - 10.0.0 - 2.0 - {E9A9D1C5-4E06-4D31-9809-A97188C70B2C} - {6BC8ED88-2882-458C-8E55-DFD12B67127B};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - Library - RestSharp - 3.2 - 3.0 - v1.0 - RestSharp.MonoTouch - - - true - full - false - bin\iPhoneSimulator\Debug - DEBUG;MONOTOUCH;FRAMEWORK - prompt - 4 - True - false - None - false - ARMv6 - false - false - AllRules.ruleset - - - none - false - bin\iPhoneSimulator\Release - prompt - 4 - False - false - None - MONOTOUCH;FRAMEWORK - false - ARMv6 - false - false - AllRules.ruleset - - - true - full - false - bin\iPhone\Debug - DEBUG;MONOTOUCH;FRAMEWORK - prompt - 4 - True - false - iPhone Developer - false - ARMv6 - false - false - AllRules.ruleset - - - none - false - bin\iPhone\Release - prompt - 4 - False - false - iPhone Developer - MONOTOUCH;FRAMEWORK - false - ARMv6 - false - false - AllRules.ruleset - - - - - - - - - - - - - Enum.cs - - - FileParameter.cs - - - Http.Async.cs - - - Http.cs - - - Http.Sync.cs - - - HttpCookie.cs - - - HttpFile.cs - - - HttpHeader.cs - - - HttpParameter.cs - - - HttpResponse.cs - - - IHttp.cs - - - IRestClient.cs - - - Parameter.cs - - - RestClient.Async.cs - - - RestClient.cs - - - RestClient.Sync.cs - - - RestClientExtensions.cs - - - RestRequest.cs - - - RestResponse.cs - - - SimpleJson.cs - - - Validation\Require.cs - - - Validation\Validate.cs - - - Serializers\ISerializer.cs - - - Serializers\JsonSerializer.cs - - - Serializers\SerializeAsAttribute.cs - - - Serializers\DotNetXmlSerializer.cs - - - Serializers\XmlSerializer.cs - - - Extensions\MiscExtensions.cs - - - Extensions\ReflectionExtensions.cs - - - Extensions\XmlExtensions.cs - - - Extensions\ResponseExtensions.cs - - - Deserializers\DeserializeAsAttribute.cs - - - Deserializers\IDeserializer.cs - - - Deserializers\JsonDeserializer.cs - - - Deserializers\XmlAttributeDeserializer.cs - - - Deserializers\XmlDeserializer.cs - - - Authenticators\HttpBasicAuthenticator.cs - - - Authenticators\IAuthenticator.cs - - - Authenticators\NtlmAuthenticator.cs - - - Authenticators\SimpleAuthenticator.cs - - - Extensions\StringExtensions.cs - - - Extensions\HttpUtility.cs - - - Extensions\HtmlEncoder.cs - - - Extensions\Helpers.cs - - - IHttpResponse.cs - - - IRestResponse.cs - - - IRestRequest.cs - - - RestResponseCookie.cs - - - IHttpFactory.cs - - - Authenticators\OAuth1Authenticator.cs - - - Authenticators\OAuth2Authenticator.cs - - - Authenticators\OAuth\HttpPostParameter.cs - - - Authenticators\OAuth\HttpPostParameterType.cs - - - Authenticators\OAuth\OAuthParameterHandling.cs - - - Authenticators\OAuth\OAuthSignatureMethod.cs - - - Authenticators\OAuth\OAuthSignatureTreatment.cs - - - Authenticators\OAuth\OAuthTools.cs - - - Authenticators\OAuth\OAuthType.cs - - - Authenticators\OAuth\OAuthWebQueryInfo.cs - - - Authenticators\OAuth\OAuthWorkflow.cs - - - Authenticators\OAuth\WebPair.cs - - - Authenticators\OAuth\WebPairCollection.cs - - - Authenticators\OAuth\WebParameter.cs - - - Authenticators\OAuth\WebParameterCollection.cs - - - Authenticators\OAuth\Extensions\CollectionExtensions.cs - - - Authenticators\OAuth\Extensions\OAuthExtensions.cs - - - Authenticators\OAuth\Extensions\StringExtensions.cs - - - Authenticators\OAuth\Extensions\TimeExtensions.cs - - - RestRequestAsyncHandle.cs - - - Deserializers\DotNetXmlDeserializer.cs - - - SharedAssemblyInfo.cs - - - - - \ No newline at end of file diff --git a/RestSharp.MonoTouch/RestSharp.MonoTouch.sln b/RestSharp.MonoTouch/RestSharp.MonoTouch.sln deleted file mode 100644 index af771cecd..000000000 --- a/RestSharp.MonoTouch/RestSharp.MonoTouch.sln +++ /dev/null @@ -1,29 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 11.00 -# Visual Studio 2010 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestSharp.MonoTouch", "RestSharp.MonoTouch.csproj", "{E9A9D1C5-4E06-4D31-9809-A97188C70B2C}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|iPhone = Debug|iPhone - Debug|iPhoneSimulator = Debug|iPhoneSimulator - Release|iPhone = Release|iPhone - Release|iPhoneSimulator = Release|iPhoneSimulator - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {E9A9D1C5-4E06-4D31-9809-A97188C70B2C}.Debug|iPhone.ActiveCfg = Debug|iPhone - {E9A9D1C5-4E06-4D31-9809-A97188C70B2C}.Debug|iPhone.Build.0 = Debug|iPhone - {E9A9D1C5-4E06-4D31-9809-A97188C70B2C}.Debug|iPhoneSimulator.ActiveCfg = Debug|iPhoneSimulator - {E9A9D1C5-4E06-4D31-9809-A97188C70B2C}.Debug|iPhoneSimulator.Build.0 = Debug|iPhoneSimulator - {E9A9D1C5-4E06-4D31-9809-A97188C70B2C}.Release|iPhone.ActiveCfg = Release|iPhone - {E9A9D1C5-4E06-4D31-9809-A97188C70B2C}.Release|iPhone.Build.0 = Release|iPhone - {E9A9D1C5-4E06-4D31-9809-A97188C70B2C}.Release|iPhoneSimulator.ActiveCfg = Release|iPhoneSimulator - {E9A9D1C5-4E06-4D31-9809-A97188C70B2C}.Release|iPhoneSimulator.Build.0 = Release|iPhoneSimulator - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(MonoDevelopProperties) = preSolution - StartupItem = RestSharp.MonoTouch.csproj - EndGlobalSection -EndGlobal diff --git a/RestSharp.Net4/Extensions/ResponseStatusExtensions.cs b/RestSharp.Net4/Extensions/ResponseStatusExtensions.cs deleted file mode 100644 index 9ad41bcca..000000000 --- a/RestSharp.Net4/Extensions/ResponseStatusExtensions.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Net; - -namespace RestSharp.Extensions -{ - public static class ResponseStatusExtensions - { - /// - /// Convert a to a instance. - /// - /// The response status. - /// - /// responseStatus - public static WebException ToWebException(this ResponseStatus responseStatus) - { - switch (responseStatus) - { - case ResponseStatus.None: - return new WebException("The request could not be processed.", WebExceptionStatus.ServerProtocolViolation); - case ResponseStatus.Error: - return new WebException("An error occurred while processing the request.", WebExceptionStatus.ServerProtocolViolation); - case ResponseStatus.TimedOut: - return new WebException("The request timed-out.", WebExceptionStatus.Timeout); - case ResponseStatus.Aborted: - return new WebException("The request was aborted.", WebExceptionStatus.Timeout); - default: - throw new ArgumentOutOfRangeException("responseStatus"); - } - } - } -} diff --git a/RestSharp.Net4/Extensions/RestClientExtensions.cs b/RestSharp.Net4/Extensions/RestClientExtensions.cs deleted file mode 100644 index 7a135a50d..000000000 --- a/RestSharp.Net4/Extensions/RestClientExtensions.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace RestSharp -{ - public static partial class RestClientExtensions - { - public static RestResponse ExecuteDynamic(this IRestClient client, IRestRequest request) - { - var response = client.Execute(request); - - var generic = (RestResponse)response; - dynamic content = SimpleJson.DeserializeObject(response.Content); - generic.Data = content; - - return generic; - } - } -} diff --git a/RestSharp.Net4/Properties/AssemblyInfo.cs b/RestSharp.Net4/Properties/AssemblyInfo.cs deleted file mode 100644 index 279304fcd..000000000 --- a/RestSharp.Net4/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("RestSharp")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("97044cbf-6c9d-4b08-87e3-bf30fbde1933")] - -[assembly: InternalsVisibleTo("RestSharp.IntegrationTests")] \ No newline at end of file diff --git a/RestSharp.Net4/RestSharp.Net4.csproj b/RestSharp.Net4/RestSharp.Net4.csproj deleted file mode 100644 index 8601e79e6..000000000 --- a/RestSharp.Net4/RestSharp.Net4.csproj +++ /dev/null @@ -1,280 +0,0 @@ - - - - Debug - AnyCPU - 8.0.30703 - 2.0 - {5FF943A5-260F-4042-B4CE-C4977BAD4EBB} - Library - Properties - RestSharp - RestSharp - v4.0 - 512 - ..\ - true - - - true - full - false - bin\Debug\ - TRACE;DEBUG;FRAMEWORK, NET4 - prompt - 4 - bin\Debug\RestSharp.xml - - - pdbonly - true - bin\Release\ - TRACE;FRAMEWORK, NET4 - prompt - 4 - bin\Release\RestSharp.xml - 1591,1573,1658,1584,1574,1572 - true - - - - - - - - - - - - - Authenticators\HttpBasicAuthenticator.cs - - - Authenticators\IAuthenticator.cs - - - Authenticators\NtlmAuthenticator.cs - - - Authenticators\OAuth1Authenticator.cs - - - Authenticators\OAuth2Authenticator.cs - - - Authenticators\OAuth\Extensions\CollectionExtensions.cs - - - Authenticators\OAuth\Extensions\OAuthExtensions.cs - - - Authenticators\OAuth\Extensions\StringExtensions.cs - - - Authenticators\OAuth\Extensions\TimeExtensions.cs - - - Authenticators\OAuth\HttpPostParameter.cs - - - Authenticators\OAuth\HttpPostParameterType.cs - - - Authenticators\OAuth\OAuthParameterHandling.cs - - - Authenticators\OAuth\OAuthSignatureMethod.cs - - - Authenticators\OAuth\OAuthSignatureTreatment.cs - - - Authenticators\OAuth\OAuthTools.cs - - - Authenticators\OAuth\OAuthType.cs - - - Authenticators\OAuth\OAuthWebQueryInfo.cs - - - Authenticators\OAuth\OAuthWorkflow.cs - - - Authenticators\OAuth\WebPair.cs - - - Authenticators\OAuth\WebPairCollection.cs - - - Authenticators\OAuth\WebParameter.cs - - - Authenticators\OAuth\WebParameterCollection.cs - - - Authenticators\SimpleAuthenticator.cs - - - Deserializers\DeserializeAsAttribute.cs - - - Deserializers\DotNetXmlDeserializer.cs - - - Deserializers\IDeserializer.cs - - - Deserializers\JsonDeserializer.cs - - - Deserializers\XmlAttributeDeserializer.cs - - - Deserializers\XmlDeserializer.cs - - - Enum.cs - - - Extensions\MiscExtensions.cs - - - Extensions\MonoHttp\Helpers.cs - - - Extensions\MonoHttp\HtmlEncoder.cs - - - Extensions\MonoHttp\HttpUtility.cs - - - Extensions\ReflectionExtensions.cs - - - Extensions\ResponseExtensions.cs - - - Extensions\StringExtensions.cs - - - Extensions\XmlExtensions.cs - - - FileParameter.cs - - - Http.Async.cs - - - Http.cs - - - Http.Sync.cs - - - HttpCookie.cs - - - HttpFile.cs - - - HttpHeader.cs - - - HttpParameter.cs - - - HttpResponse.cs - - - IHttp.cs - - - IHttpFactory.cs - - - IHttpResponse.cs - - - IRestClient.cs - - - IRestRequest.cs - - - IRestResponse.cs - - - Parameter.cs - - - RestClient.Async.cs - - - RestClient.cs - - - RestClient.Sync.cs - - - RestClientExtensions.cs - - - RestRequest.cs - - - RestRequestAsyncHandle.cs - - - RestResponse.cs - - - RestResponseCookie.cs - - - Serializers\DotNetXmlSerializer.cs - - - Serializers\ISerializer.cs - - - Serializers\JsonSerializer.cs - - - Serializers\SerializeAsAttribute.cs - - - Serializers\XmlSerializer.cs - - - SharedAssemblyInfo.cs - - - Validation\Require.cs - - - SimpleJson.cs - - - Validation\Validate.cs - - - - - - - - Designer - - - - - - \ No newline at end of file diff --git a/RestSharp.Net4/packages.config b/RestSharp.Net4/packages.config deleted file mode 100644 index 6b8deb9c9..000000000 --- a/RestSharp.Net4/packages.config +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/RestSharp.Silverlight/Properties/AssemblyInfo.cs b/RestSharp.Silverlight/Properties/AssemblyInfo.cs deleted file mode 100644 index 14455cf8c..000000000 --- a/RestSharp.Silverlight/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("RestSharp.Silverlight")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("2314f291-c979-46b1-932c-f3ae123a3cef")] \ No newline at end of file diff --git a/RestSharp.Silverlight/RestSharp.Silverlight.csproj b/RestSharp.Silverlight/RestSharp.Silverlight.csproj deleted file mode 100644 index 0d272963f..000000000 --- a/RestSharp.Silverlight/RestSharp.Silverlight.csproj +++ /dev/null @@ -1,288 +0,0 @@ - - - - Debug - AnyCPU - 8.0.50727 - 2.0 - {11F84600-0978-48B9-A28F-63B3781E54B3} - {A1591282-1198-4647-A2B1-27E5FF5F6F3B};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} - Library - Properties - RestSharp.Silverlight - RestSharp.Silverlight - Silverlight - v4.0 - $(TargetFrameworkVersion) - false - true - true - ..\ - true - - - - v3.5 - - - true - full - false - Bin\Debug - DEBUG;TRACE;SILVERLIGHT - true - true - prompt - 4 - Bin\Debug\RestSharp.Silverlight.xml - - - pdbonly - true - Bin\Release - TRACE;SILVERLIGHT - true - true - prompt - 4 - Bin\Release\RestSharp.Silverlight.xml - 1591,1573,1658,1584,1574,1572 - true - - - - - - - - - - - - - - - - - Authenticators\HttpBasicAuthenticator.cs - - - Authenticators\IAuthenticator.cs - - - Authenticators\NtlmAuthenticator.cs - - - Authenticators\OAuth1Authenticator.cs - - - Authenticators\OAuth2Authenticator.cs - - - Authenticators\OAuth\Extensions\CollectionExtensions.cs - - - Authenticators\OAuth\Extensions\OAuthExtensions.cs - - - Authenticators\OAuth\Extensions\StringExtensions.cs - - - Authenticators\OAuth\Extensions\TimeExtensions.cs - - - Authenticators\OAuth\HttpPostParameter.cs - - - Authenticators\OAuth\HttpPostParameterType.cs - - - Authenticators\OAuth\OAuthParameterHandling.cs - - - Authenticators\OAuth\OAuthSignatureMethod.cs - - - Authenticators\OAuth\OAuthSignatureTreatment.cs - - - Authenticators\OAuth\OAuthTools.cs - - - Authenticators\OAuth\OAuthType.cs - - - Authenticators\OAuth\OAuthWebQueryInfo.cs - - - Authenticators\OAuth\OAuthWorkflow.cs - - - Authenticators\OAuth\WebPair.cs - - - Authenticators\OAuth\WebPairCollection.cs - - - Authenticators\OAuth\WebParameter.cs - - - Authenticators\OAuth\WebParameterCollection.cs - - - Authenticators\SimpleAuthenticator.cs - - - Deserializers\DeserializeAsAttribute.cs - - - Deserializers\DotNetXmlDeserializer.cs - - - Deserializers\IDeserializer.cs - - - Deserializers\JsonDeserializer.cs - - - Deserializers\XmlAttributeDeserializer.cs - - - Deserializers\XmlDeserializer.cs - - - Enum.cs - - - Extensions\MiscExtensions.cs - - - Extensions\ReflectionExtensions.cs - - - Extensions\ResponseExtensions.cs - - - Extensions\StringExtensions.cs - - - Extensions\XmlExtensions.cs - - - FileParameter.cs - - - Http.Async.cs - - - Http.cs - - - HttpCookie.cs - - - HttpFile.cs - - - HttpHeader.cs - - - HttpParameter.cs - - - HttpResponse.cs - - - IHttp.cs - - - IHttpFactory.cs - - - IHttpResponse.cs - - - IRestClient.cs - - - IRestRequest.cs - - - IRestResponse.cs - - - Parameter.cs - - - RestClient.Async.cs - - - RestClient.cs - - - RestClientExtensions.cs - - - RestRequest.cs - - - RestResponse.cs - - - Serializers\DotNetXmlSerializer.cs - - - Serializers\ISerializer.cs - - - Serializers\JsonSerializer.cs - - - Serializers\SerializeAsAttribute.cs - - - Serializers\XmlSerializer.cs - - - SharedAssemblyInfo.cs - - - SimpleJson.cs - - - Validation\Require.cs - - - Validation\Validate.cs - - - RestResponseCookie.cs - - - RestRequestAsyncHandle.cs - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/RestSharp.Silverlight/packages.config b/RestSharp.Silverlight/packages.config deleted file mode 100644 index 6b8deb9c9..000000000 --- a/RestSharp.Silverlight/packages.config +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/RestSharp.Tests/CultureChange.cs b/RestSharp.Tests/CultureChange.cs deleted file mode 100644 index abd5c6fd3..000000000 --- a/RestSharp.Tests/CultureChange.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using System.Globalization; -using System.Threading; - -namespace RestSharp.Tests -{ - public class CultureChange : IDisposable - { - public CultureInfo PreviousCulture { get; private set; } - - public CultureChange(string culture) - { - if (culture == null) - throw new ArgumentNullException("culture"); - - PreviousCulture = Thread.CurrentThread.CurrentCulture; - - Thread.CurrentThread.CurrentCulture = new CultureInfo(culture); - } - - #region IDisposable Members - - public void Dispose() - { - if (PreviousCulture != null) - { - Thread.CurrentThread.CurrentCulture = PreviousCulture; - - PreviousCulture = null; - } - } - - #endregion - } -} \ No newline at end of file diff --git a/RestSharp.Tests/Fakes/NullHttp.cs b/RestSharp.Tests/Fakes/NullHttp.cs deleted file mode 100644 index 76a2b6f5e..000000000 --- a/RestSharp.Tests/Fakes/NullHttp.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace RestSharp.Tests.Fakes -{ - public class NullHttp : Http - { - public new HttpResponse Get() - { - return new HttpResponse(); - } - } -} diff --git a/RestSharp.Tests/JsonTests.cs b/RestSharp.Tests/JsonTests.cs deleted file mode 100644 index 4d8ce03be..000000000 --- a/RestSharp.Tests/JsonTests.cs +++ /dev/null @@ -1,910 +0,0 @@ -#region License -// Copyright 2010 John Sheehan -// -// 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. -#endregion - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Globalization; -using System.IO; -using System.Linq; -using RestSharp.Deserializers; -using RestSharp.Tests.SampleClasses; -using Xunit; - -namespace RestSharp.Tests -{ - public class JsonTests - { - private const string AlternativeCulture = "pt-PT"; - - private const string GuidString = "AC1FC4BC-087A-4242-B8EE-C53EBE9887A5"; - - [Fact] - public void Can_Deserialize_Select_Tokens() - { - var data = File.ReadAllText(Path.Combine("SampleData", "jsonarray.txt")); - var response = new RestResponse { Content = data }; - var json = new JsonDeserializer(); - var output = json.Deserialize(response); - Assert.Equal(4, output.Count); - } - - [Fact] - public void Can_Deserialize_4sq_Json_With_Root_Element_Specified() - { - var doc = File.ReadAllText(Path.Combine("SampleData", "4sq.txt")); - - var json = new JsonDeserializer(); - json.RootElement = "response"; - - var output = json.Deserialize(new RestResponse { Content = doc }); - - Assert.NotEmpty(output.Groups); - } - - [Fact] - public void Can_Deserialize_Lists_of_Simple_Types() - { - var doc = File.ReadAllText(Path.Combine("SampleData", "jsonlists.txt")); - var json = new JsonDeserializer (); - - var output = json.Deserialize (new RestResponse { Content = doc }); - - Assert.NotEmpty (output.Names); - Assert.NotEmpty (output.Numbers); - } - - [Fact] - public void Can_Deserialize_Simple_Generic_List_of_Simple_Types() - { - const string content = "{\"users\":[\"johnsheehan\",\"jagregory\",\"drusellers\",\"structuremap\"]}"; - var json = new JsonDeserializer {RootElement = "users"}; - - var output = json.Deserialize>(new RestResponse {Content = content}); - - Assert.NotEmpty(output); - } - - [Fact] - public void Can_Deserialize_Simple_Generic_List_of_Simple_Types_With_Nulls () - { - const string content = "{\"users\":[\"johnsheehan\",\"jagregory\",null,\"drusellers\",\"structuremap\"]}"; - var json = new JsonDeserializer { RootElement = "users" }; - - var output = json.Deserialize> (new RestResponse { Content = content }); - - Assert.NotEmpty (output); - Assert.Equal (null, output[2]); - Assert.Equal (5, output.Count); - } - - [Fact] - public void Can_Deserialize_Simple_Generic_List_Given_Item_Without_Array () - { - const string content = "{\"users\":\"johnsheehan\"}"; - var json = new JsonDeserializer { RootElement = "users" }; - - var output = json.Deserialize> (new RestResponse { Content = content }); - - Assert.True (output.SequenceEqual (new[] { "johnsheehan" })); - } - - [Fact] - public void Can_Deserialize_Simple_Generic_List_Given_Toplevel_Item_Without_Array () - { - const string content = "\"johnsheehan\""; - var json = new JsonDeserializer (); - - var output = json.Deserialize> (new RestResponse { Content = content }); - - Assert.True (output.SequenceEqual (new[] { "johnsheehan" })); - } - - [Fact] - public void Can_Deserialize_From_Root_Element() - { - var doc = File.ReadAllText(Path.Combine("SampleData", "sojson.txt")); - - var json = new JsonDeserializer(); - json.RootElement = "User"; - - var output = json.Deserialize(new RestResponse { Content = doc }); - Assert.Equal("John Sheehan", output.DisplayName); - } - - [Fact] - public void Can_Deserialize_Generic_Members() - { - var doc = File.ReadAllText(Path.Combine("SampleData", "GenericWithList.txt")); - var json = new JsonDeserializer(); - - var output = json.Deserialize>>(new RestResponse { Content = doc }); - Assert.Equal("Foe sho", output.Data.Items[0].Nickname); - } - - [Fact] - public void Can_Deserialize_List_of_Guid() - { - Guid ID1 = new Guid("b0e5c11f-e944-478c-aadd-753b956d0c8c"); - Guid ID2 = new Guid("809399fa-21c4-4dca-8dcd-34cb697fbca0"); - var data = new JsonObject(); - data["Ids"] = new JsonArray() {ID1, ID2}; - - var d = new JsonDeserializer(); - var response = new RestResponse { Content = data.ToString() }; - var p = d.Deserialize(response); - - Assert.Equal(2, p.Ids.Count); - Assert.Equal(ID1, p.Ids[0]); - Assert.Equal(ID2, p.Ids[1]); - } - - [Fact] - public void Can_Deserialize_Generic_List_of_DateTime() - { - DateTime Item1 = new DateTime(2010, 2, 8, 11, 11, 11); - DateTime Item2 = Item1.AddSeconds(12345); - var data = new JsonObject(); - data["Items"] = new JsonArray {Item1.ToString("u"), Item2.ToString("u")}; - - var d = new JsonDeserializer(); - var response = new RestResponse { Content = data.ToString() }; - var p = d.Deserialize>(response); - - Assert.Equal(2, p.Items.Count); - Assert.Equal(Item1, p.Items[0]); - Assert.Equal(Item2, p.Items[1]); - } - - [Fact] - public void Can_Deserialize_Null_Elements_to_Nullable_Values() - { - var doc = CreateJsonWithNullValues(); - - var json = new JsonDeserializer(); - var output = json.Deserialize(new RestResponse { Content = doc }); - - Assert.Null(output.Id); - Assert.Null(output.StartDate); - Assert.Null(output.UniqueId); - } - - [Fact] - public void Can_Deserialize_Empty_Elements_to_Nullable_Values() - { - var doc = CreateJsonWithEmptyValues(); - - var json = new JsonDeserializer(); - var output = json.Deserialize(new RestResponse { Content = doc }); - - Assert.Null(output.Id); - Assert.Null(output.StartDate); - Assert.Null(output.UniqueId); - } - - [Fact] - public void Can_Deserialize_Elements_to_Nullable_Values() - { - var doc = CreateJsonWithoutEmptyValues(); - - var json = new JsonDeserializer(); - var output = json.Deserialize(new RestResponse { Content = doc }); - - Assert.NotNull(output.Id); - Assert.NotNull(output.StartDate); - Assert.NotNull(output.UniqueId); - - Assert.Equal(123, output.Id); - Assert.NotNull(output.StartDate); - Assert.Equal( - new DateTime(2010, 2, 21, 9, 35, 00, DateTimeKind.Utc), - output.StartDate.Value); - Assert.Equal(new Guid(GuidString), output.UniqueId); - } - - [Fact] - public void Can_Deserialize_Json_Using_DeserializeAs_Attribute() - { - const string content = "{\"sid\":\"asdasdasdasdasdasdasda\",\"friendlyName\":\"VeryNiceName\",\"oddballPropertyName\":\"blahblah\"}"; - - var json = new JsonDeserializer { RootElement = "users" }; - var output = json.Deserialize(new RestResponse { Content = content }); - - Assert.NotNull(output); - Assert.Equal("blahblah", output.GoodPropertyName); - } - - [Fact] - public void Can_Deserialize_Custom_Formatted_Date() - { - var culture = CultureInfo.InvariantCulture; - var format = "dd yyyy MMM, hh:mm ss tt"; - var date = new DateTime(2010, 2, 8, 11, 11, 11); - - var formatted = new - { - StartDate = date.ToString(format, culture) - }; - - var data = SimpleJson.SerializeObject(formatted); - var response = new RestResponse { Content = data }; - - var json = new JsonDeserializer { DateFormat = format, Culture = culture }; - - var output = json.Deserialize(response); - - Assert.Equal(date, output.StartDate); - } - - [Fact] - public void Can_Deserialize_Root_Json_Array_To_List() - { - var data = File.ReadAllText(Path.Combine("SampleData", "jsonarray.txt")); - var response = new RestResponse { Content = data }; - var json = new JsonDeserializer(); - var output = json.Deserialize>(response); - Assert.Equal(4, output.Count); - } - - [Fact] - public void Can_Deserialize_Root_Json_Array_To_Inherited_List() - { - var data = File.ReadAllText(Path.Combine("SampleData", "jsonarray.txt")); - var response = new RestResponse { Content = data }; - var json = new JsonDeserializer(); - var output = json.Deserialize(response); - Assert.Equal(4, output.Count); - } - - [Fact] - public void Can_Deserialize_Various_Enum_Values () - { - var data = File.ReadAllText (Path.Combine ("SampleData", "jsonenums.txt")); - var response = new RestResponse { Content = data }; - var json = new JsonDeserializer (); - var output = json.Deserialize(response); - - Assert.Equal(Disposition.Friendly,output.Upper); - Assert.Equal(Disposition.Friendly,output.Lower); - Assert.Equal(Disposition.SoSo,output.CamelCased); - Assert.Equal(Disposition.SoSo,output.Underscores); - Assert.Equal(Disposition.SoSo,output.LowerUnderscores); - Assert.Equal(Disposition.SoSo,output.Dashes); - Assert.Equal(Disposition.SoSo,output.LowerDashes); - Assert.Equal(Disposition.SoSo,output.Integer); - } - - [Fact] - public void Can_Deserialize_Various_Enum_Types() - { - var data = File.ReadAllText(Path.Combine("SampleData", "jsonenumtypes.txt")); - var response = new RestResponse {Content = data}; - var json = new JsonDeserializer(); - var output = json.Deserialize(response); - - Assert.Equal(ByteEnum.EnumMin, output.ByteEnumType); - Assert.Equal(SByteEnum.EnumMin, output.SByteEnumType); - Assert.Equal(ShortEnum.EnumMin, output.ShortEnumType); - Assert.Equal(UShortEnum.EnumMin, output.UShortEnumType); - Assert.Equal(IntEnum.EnumMin, output.IntEnumType); - Assert.Equal(UIntEnum.EnumMin, output.UIntEnumType); - Assert.Equal(LongEnum.EnumMin, output.LongEnumType); - Assert.Equal(ULongEnum.EnumMin, output.ULongEnumType); - } - - [Fact] - public void Deserialization_Of_Undefined_Int_Value_Returns_Enum_Default() - { - const string data = @"{ ""Integer"" : 1024 }"; - var response = new RestResponse { Content = data }; - var json = new JsonDeserializer (); - var result = json.Deserialize(response); - Assert.Equal(Disposition.Friendly,result.Integer); - } - - [Fact] - public void Can_Deserialize_Guid_String_Fields() - { - var doc = new JsonObject(); - doc["Guid"] = GuidString; - - var d = new JsonDeserializer(); - var response = new RestResponse { Content = doc.ToString() }; - var p = d.Deserialize(response); - - Assert.Equal(new Guid(GuidString), p.Guid); - } - - [Fact] - public void Can_Deserialize_Quoted_Primitive() - { - var doc = new JsonObject(); - doc["Age"] = "28"; - - var d = new JsonDeserializer(); - var response = new RestResponse { Content = doc.ToString() }; - var p = d.Deserialize(response); - - Assert.Equal(28, p.Age); - } - - [Fact] - public void Can_Deserialize_Int_to_Bool() - { - var doc = new JsonObject(); - doc["IsCool"] = 1; - - var d = new JsonDeserializer(); - var response = new RestResponse { Content = doc.ToString() }; - var p = d.Deserialize(response); - - Assert.True(p.IsCool); - } - - [Fact] - public void Can_Deserialize_With_Default_Root() - { - var doc = CreateJson(); - var d = new JsonDeserializer(); - var response = new RestResponse { Content = doc }; - var p = d.Deserialize(response); - - Assert.Equal("John Sheehan", p.Name); - Assert.Equal(new DateTime(2009, 9, 25, 0, 6, 1, DateTimeKind.Utc), p.StartDate); - Assert.Equal(28, p.Age); - Assert.Equal(long.MaxValue, p.BigNumber); - Assert.Equal(99.9999m, p.Percent); - Assert.Equal(false, p.IsCool); - Assert.Equal(new Uri("http://example.com", UriKind.RelativeOrAbsolute), p.Url); - Assert.Equal(new Uri("/foo/bar", UriKind.RelativeOrAbsolute), p.UrlPath); - - Assert.Equal(Guid.Empty, p.EmptyGuid); - Assert.Equal(new Guid(GuidString), p.Guid); - - Assert.Equal(Order.Third, p.Order); - Assert.Equal(Disposition.SoSo, p.Disposition); - - Assert.NotNull(p.Friends); - Assert.Equal(10, p.Friends.Count); - - Assert.NotNull(p.BestFriend); - Assert.Equal("The Fonz", p.BestFriend.Name); - Assert.Equal(1952, p.BestFriend.Since); - - Assert.NotEmpty(p.Foes); - Assert.Equal("Foe 1", p.Foes["dict1"].Nickname); - Assert.Equal("Foe 2", p.Foes["dict2"].Nickname); - } - - [Fact] - public void Can_Deserialize_With_Default_Root_Alternative_Culture() - { - using (new CultureChange(AlternativeCulture)) - { - Can_Deserialize_With_Default_Root(); - } - } - - [Fact] - public void Can_Deserialize_Names_With_Underscore_Prefix() - { - var data = File.ReadAllText(Path.Combine("SampleData", "underscore_prefix.txt")); - var response = new RestResponse { Content = data }; - var json = new JsonDeserializer(); - json.RootElement = "User"; - - var output = json.Deserialize(response); - - Assert.Equal("John Sheehan", output.DisplayName); - Assert.Equal(1786, output.Id); - } - - [Fact] - public void Can_Deserialize_Names_With_Underscores_With_Default_Root() - { - var doc = CreateJsonWithUnderscores(); - var d = new JsonDeserializer(); - var response = new RestResponse { Content = doc }; - var p = d.Deserialize(response); - - Assert.Equal("John Sheehan", p.Name); - Assert.Equal(new DateTime(2009, 9, 25, 0, 6, 1), p.StartDate); - Assert.Equal(28, p.Age); - Assert.Equal(long.MaxValue, p.BigNumber); - Assert.Equal(99.9999m, p.Percent); - Assert.Equal(false, p.IsCool); - Assert.Equal(new Uri("http://example.com", UriKind.RelativeOrAbsolute), p.Url); - Assert.Equal(new Uri("/foo/bar", UriKind.RelativeOrAbsolute), p.UrlPath); - - Assert.NotNull(p.Friends); - Assert.Equal(10, p.Friends.Count); - - Assert.NotNull(p.BestFriend); - Assert.Equal("The Fonz", p.BestFriend.Name); - Assert.Equal(1952, p.BestFriend.Since); - - Assert.NotEmpty(p.Foes); - Assert.Equal("Foe 1", p.Foes["dict1"].Nickname); - Assert.Equal("Foe 2", p.Foes["dict2"].Nickname); - } - - [Fact] - public void Can_Deserialize_Names_With_Underscores_With_Default_Root_Alternative_Culture() - { - using (new CultureChange(AlternativeCulture)) - { - Can_Deserialize_Names_With_Underscores_With_Default_Root(); - } - } - - [Fact] - public void Can_Deserialize_Names_With_Dashes_With_Default_Root() - { - var doc = CreateJsonWithDashes(); - var d = new JsonDeserializer(); - var response = new RestResponse { Content = doc }; - var p = d.Deserialize(response); - - Assert.Equal("John Sheehan", p.Name); - //Assert.Equal(new DateTime(2009, 9, 25, 0, 6, 1, DateTimeKind.Utc), p.StartDate); - Assert.Equal(28, p.Age); - Assert.Equal(long.MaxValue, p.BigNumber); - Assert.Equal(99.9999m, p.Percent); - Assert.Equal(false, p.IsCool); - Assert.Equal(new Uri("http://example.com", UriKind.RelativeOrAbsolute), p.Url); - Assert.Equal(new Uri("/foo/bar", UriKind.RelativeOrAbsolute), p.UrlPath); - - Assert.NotNull(p.Friends); - Assert.Equal(10, p.Friends.Count); - - Assert.NotNull(p.BestFriend); - Assert.Equal("The Fonz", p.BestFriend.Name); - Assert.Equal(1952, p.BestFriend.Since); - - Assert.NotEmpty(p.Foes); - Assert.Equal("Foe 1", p.Foes["dict1"].Nickname); - Assert.Equal("Foe 2", p.Foes["dict2"].Nickname); - } - - [Fact] - public void Can_Deserialize_Names_With_Dashes_With_Default_Root_Alternative_Culture() - { - using (new CultureChange(AlternativeCulture)) - { - Can_Deserialize_Names_With_Dashes_With_Default_Root(); - } - } - - [Fact] - public void Ignore_Protected_Property_That_Exists_In_Data() - { - var doc = CreateJson(); - var d = new JsonDeserializer(); - var response = new RestResponse { Content = doc }; - var p = d.Deserialize(response); - - Assert.Null(p.IgnoreProxy); - } - - [Fact] - public void Ignore_ReadOnly_Property_That_Exists_In_Data() - { - var doc = CreateJson(); - var response = new RestResponse { Content = doc }; - var d = new JsonDeserializer(); - var p = d.Deserialize(response); - - Assert.Null(p.ReadOnlyProxy); - } - - [Fact] - public void Can_Deserialize_TimeSpan() - { - var payload = GetPayLoad("timespans.txt"); - - Assert.Equal(new TimeSpan(468006), payload.Tick); - Assert.Equal(new TimeSpan(0, 0, 0, 0, 125), payload.Millisecond); - Assert.Equal(new TimeSpan(0, 0, 8), payload.Second); - Assert.Equal(new TimeSpan(0, 55, 2), payload.Minute); - Assert.Equal(new TimeSpan(21, 30, 7), payload.Hour); - Assert.Null(payload.NullableWithoutValue); - Assert.NotNull(payload.NullableWithValue); - Assert.Equal(new TimeSpan(21, 30, 7), payload.NullableWithValue.Value); - } - - [Fact] - public void Can_Deserialize_Iso_Json_Dates() - { - var doc = CreateIsoDateJson(); - var d = new JsonDeserializer(); - var response = new RestResponse { Content = doc }; - var bd = d.Deserialize(response); - - Assert.Equal(new DateTime(1910, 9, 25, 9, 30, 25, DateTimeKind.Utc), bd.Value); - } - - [Fact] - public void Can_Deserialize_Unix_Json_Dates() - { - var doc = CreateUnixDateJson(); - var d = new JsonDeserializer(); - var response = new RestResponse { Content = doc }; - var bd = d.Deserialize(response); - - Assert.Equal(new DateTime(2011, 6, 30, 8, 15, 46, DateTimeKind.Utc), bd.Value); - } - - [Fact] - public void Can_Deserialize_JsonNet_Dates() - { - var person = GetPayLoad("person.json.txt"); - - Assert.Equal( - new DateTime(2011, 6, 30, 8, 15, 46, 929, DateTimeKind.Utc), - person.StartDate); - } - - [Fact] - public void Can_Deserialize_DateTime() - { - var payload = GetPayLoad("datetimes.txt"); - - Assert.Equal( - new DateTime(2011, 6, 30, 8, 15, 46, 929, DateTimeKind.Utc), - payload.DateTime); - } - - [Fact] - public void Can_Deserialize_Nullable_DateTime_With_Value() - { - var payload = GetPayLoad("datetimes.txt"); - - Assert.NotNull(payload.NullableDateTimeWithValue); - Assert.Equal( - new DateTime(2011, 6, 30, 8, 15, 46, 929, DateTimeKind.Utc), - payload.NullableDateTimeWithValue.Value); - } - - [Fact] - public void Can_Deserialize_Nullable_DateTime_With_Null() - { - var payload = GetPayLoad("datetimes.txt"); - - Assert.Null(payload.NullableDateTimeWithNull); - } - - [Fact] - public void Can_Deserialize_DateTimeOffset() - { - var payload = GetPayLoad("datetimes.txt"); - - Assert.Equal( - new DateTime(2011, 6, 30, 8, 15, 46, 929, DateTimeKind.Utc), - payload.DateTimeOffset); - } - - [Fact] - public void Can_Deserialize_Iso8601DateTimeLocal() - { - var payload = GetPayLoad("iso8601datetimes.txt"); - - Assert.Equal( - new DateTime(2012, 7, 19, 10, 23, 25, DateTimeKind.Utc), - payload.DateTimeLocal); - } - - [Fact] - public void Can_Deserialize_Iso8601DateTimeZulu() - { - var payload = GetPayLoad("iso8601datetimes.txt"); - - Assert.Equal( - new DateTime(2012, 7, 19, 10, 23, 25, 544, DateTimeKind.Utc), - payload.DateTimeUtc.ToUniversalTime()); - } - - [Fact] - public void Can_Deserialize_Iso8601DateTimeWithOffset() - { - var payload = GetPayLoad("iso8601datetimes.txt"); - - Assert.Equal( - new DateTime(2012, 7, 19, 10, 23, 25, 544, DateTimeKind.Utc), - payload.DateTimeWithOffset.ToUniversalTime()); - } - - [Fact] - public void Can_Deserialize_Nullable_DateTimeOffset_With_Value() - { - var payload = GetPayLoad("datetimes.txt"); - - Assert.NotNull(payload.NullableDateTimeOffsetWithValue); - Assert.Equal( - new DateTime(2011, 6, 30, 8, 15, 46, 929, DateTimeKind.Utc), - payload.NullableDateTimeOffsetWithValue); - } - - [Fact] - public void Can_Deserialize_Nullable_DateTimeOffset_With_Null() - { - var payload = GetPayLoad("datetimes.txt"); - - Assert.Null(payload.NullableDateTimeOffsetWithNull); - } - - [Fact] - public void Can_Deserialize_To_Dictionary_String_String() - { - var doc = CreateJsonStringDictionary(); - var d = new JsonDeserializer(); - var response = new RestResponse { Content = doc }; - var bd = d.Deserialize>(response); - - Assert.Equal(bd["Thing1"], "Thing1"); - Assert.Equal(bd["Thing2"], "Thing2"); - Assert.Equal(bd["ThingRed"], "ThingRed"); - Assert.Equal(bd["ThingBlue"], "ThingBlue"); - } - - [Fact] - public void Can_Deserialize_To_Dictionary_String_String_With_Dynamic_Values () - { - var doc = CreateDynamicJsonStringDictionary (); - var d = new JsonDeserializer (); - var response = new RestResponse { Content = doc }; - var bd = d.Deserialize> (response); - - Assert.Equal ("[\"Value1\",\"Value2\"]", bd["Thing1"]); - Assert.Equal ("Thing2", bd["Thing2"]); - Assert.Equal ("{\"Name\":\"ThingRed\",\"Color\":\"Red\"}", bd["ThingRed"]); - Assert.Equal ("{\"Name\":\"ThingBlue\",\"Color\":\"Blue\"}", bd["ThingBlue"]); - } - - [Fact] - public void Can_Deserialize_Decimal_With_Four_Zeros_After_Floating_Point() - { - const string json = "{\"Value\":0.00005557}"; - var response = new RestResponse() {Content = json}; - var d = new JsonDeserializer(); - var result = d.Deserialize(response); - - Assert.Equal(result.Value, .00005557m); - } - - [Fact] - public void Can_Deserialize_Object_Type_Property_With_Primitive_Vale() - { - var payload = GetPayLoad("objectproperty.txt"); - - Assert.Equal(42L, payload.ObjectProperty); - } - - [Fact] - public void Can_Deserialize_Dictionary_of_Lists() - { - var doc = File.ReadAllText(Path.Combine("SampleData", "jsondictionary.txt")); - - var json = new JsonDeserializer(); - json.RootElement = "response"; - - var output = json.Deserialize(new RestResponse { Content = doc }); - - Assert.NotEmpty(output.EmployeesMail); - Assert.NotEmpty(output.EmployeesTime); - Assert.NotEmpty(output.EmployeesPay); - } - - private string CreateJsonWithUnderscores() - { - var doc = new JsonObject(); - doc["name"] = "John Sheehan"; - doc["start_date"] = new DateTime(2009, 9, 25, 0, 6, 1, DateTimeKind.Utc); - doc["age"] = 28; - doc["percent"] = 99.9999m; - doc["big_number"] = long.MaxValue; - doc["is_cool"] = false; - doc["ignore"] = "dummy"; - doc["read_only"] = "dummy"; - doc["url"] = "http://example.com"; - doc["url_path"] = "/foo/bar"; - - - doc["best_friend"] = new JsonObject { - {"name", "The Fonz"}, - {"since", 1952} - }; - - var friendsArray = new JsonArray(); - for (int i = 0; i < 10; i++) - { - friendsArray.Add(new JsonObject { - {"name", "Friend" + i}, - {"since", DateTime.Now.Year - i} - }); - } - - doc["friends"] = friendsArray; - - var foesArray = new JsonObject{ - {"dict1", new JsonObject{{"nickname", "Foe 1"}}}, - {"dict2", new JsonObject{{"nickname", "Foe 2"}}} - }; - - doc["foes"] = foesArray; - - return doc.ToString(); - } - - private string CreateJsonWithDashes() - { - var doc = new JsonObject(); - doc["name"] = "John Sheehan"; - doc["start-date"] = new DateTime(2009, 9, 25, 0, 6, 1, DateTimeKind.Utc); - doc["age"] = 28; - doc["percent"] = 99.9999m; - doc["big-number"] = long.MaxValue; - doc["is-cool"] = false; - doc["ignore"] = "dummy"; - doc["read-only"] = "dummy"; - doc["url"] = "http://example.com"; - doc["url-path"] = "/foo/bar"; - - doc["best-friend"] = new JsonObject{ - {"name", "The Fonz"}, - {"since", 1952} - }; - - var friendsArray = new JsonArray(); - for (int i = 0; i < 10; i++) - { - friendsArray.Add(new JsonObject{ - {"name", "Friend" + i}, - {"since", DateTime.Now.Year - i} - }); - } - - doc["friends"] = friendsArray; - - var foesArray = new JsonObject{ - {"dict1", new JsonObject{{"nickname", "Foe 1"}}}, - {"dict2", new JsonObject{{"nickname", "Foe 2"}}} - }; - - doc["foes"] = foesArray; - - return doc.ToString(); - } - - private string CreateIsoDateJson() - { - var bd = new Birthdate(); - bd.Value = new DateTime(1910, 9, 25, 9, 30, 25, DateTimeKind.Utc); - - return SimpleJson.SerializeObject(bd); - } - - private string CreateUnixDateJson() - { - var doc = new JsonObject(); - doc["Value"] = 1309421746; - - return doc.ToString(); - } - - private string CreateJson() - { - var doc = new JsonObject(); - doc["Name"] = "John Sheehan"; - doc["StartDate"] = new DateTime(2009, 9, 25, 0, 6, 1, DateTimeKind.Utc); - doc["Age"] = 28; - doc["Percent"] = 99.9999m; - doc["BigNumber"] = long.MaxValue; - doc["IsCool"] = false; - doc["Ignore"] = "dummy"; - doc["ReadOnly"] = "dummy"; - doc["Url"] = "http://example.com"; - doc["UrlPath"] = "/foo/bar"; - doc["Order"] = "third"; - doc["Disposition"] = "so_so"; - - doc["Guid"] = new Guid(GuidString).ToString(); - doc["EmptyGuid"] = ""; - - doc["BestFriend"] = new JsonObject{ - {"Name", "The Fonz"}, - {"Since", 1952} - }; - - var friendsArray = new JsonArray(); - for (int i = 0; i < 10; i++) - { - friendsArray.Add(new JsonObject{ - {"Name", "Friend" + i}, - {"Since", DateTime.Now.Year - i} - }); - } - - doc["Friends"] = friendsArray; - - var foesArray = new JsonObject{ - {"dict1", new JsonObject{{"Nickname", "Foe 1"}}}, - {"dict2", new JsonObject{{"Nickname", "Foe 2"}}} - }; - - doc["Foes"] = foesArray; - - return doc.ToString(); - } - - private string CreateJsonWithNullValues() - { - var doc = new JsonObject(); - doc["Id"] = null; - doc["StartDate"] = null; - doc["UniqueId"] = null; - - return doc.ToString(); - } - - private string CreateJsonWithEmptyValues() - { - var doc = new JsonObject(); - doc["Id"] = ""; - doc["StartDate"] = ""; - doc["UniqueId"] = ""; - - return doc.ToString(); - } - - private string CreateJsonWithoutEmptyValues() - { - var doc = new JsonObject(); - doc["Id"] = 123; - doc["StartDate"] = new DateTime(2010, 2, 21, 9, 35, 00, DateTimeKind.Utc); - doc["UniqueId"] = new Guid(GuidString).ToString(); - - return doc.ToString(); - } - - public string CreateJsonStringDictionary() - { - var doc = new JsonObject(); - doc["Thing1"] = "Thing1"; - doc["Thing2"] = "Thing2"; - doc["ThingRed"] = "ThingRed"; - doc["ThingBlue"] = "ThingBlue"; - return doc.ToString(); - } - - public string CreateDynamicJsonStringDictionary () - { - var doc = new JsonObject (); - doc["Thing1"] = new JsonArray { "Value1", "Value2" }; - doc["Thing2"] = "Thing2"; - doc["ThingRed"] = new JsonObject {{"Name", "ThingRed"}, {"Color", "Red"}}; - doc["ThingBlue"] = new JsonObject {{"Name", "ThingBlue"}, {"Color", "Blue"}}; - return doc.ToString (); - } - - private T GetPayLoad(string fileName) - { - var doc = File.ReadAllText(Path.Combine("SampleData", fileName)); - var response = new RestResponse { Content = doc }; - var d = new JsonDeserializer(); - return d.Deserialize(response); - } - } -} diff --git a/RestSharp.Tests/NamespacedXmlTests.cs b/RestSharp.Tests/NamespacedXmlTests.cs deleted file mode 100644 index cd08ea8e6..000000000 --- a/RestSharp.Tests/NamespacedXmlTests.cs +++ /dev/null @@ -1,287 +0,0 @@ -#region Licensed -// Copyright 2010 John Sheehan -// -// 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. -#endregion - -using System; -using System.Collections.Generic; -using System.Xml.Linq; -using RestSharp.Deserializers; -using RestSharp.Tests.SampleClasses.Lastfm; -using Xunit; - -namespace RestSharp.Tests -{ - public class NamespacedXmlTests - { - private const string GuidString = "AC1FC4BC-087A-4242-B8EE-C53EBE9887A5"; - - [Fact] - public void Can_Deserialize_Elements_With_Namespace() { - var doc = CreateElementsXml(); - - var response = new RestResponse { Content = doc }; - - var d = new XmlDeserializer(); - d.Namespace = "http://restsharp.org"; - var p = d.Deserialize(response); - - Assert.Equal("John Sheehan", p.Name); - Assert.Equal(new DateTime(2009, 9, 25, 0, 6, 1), p.StartDate); - Assert.Equal(28, p.Age); - Assert.Equal(long.MaxValue, p.BigNumber); - Assert.Equal(99.9999m, p.Percent); - Assert.Equal(false, p.IsCool); - Assert.Equal(new Guid(GuidString), p.UniqueId); - Assert.Equal(new Uri("http://example.com", UriKind.RelativeOrAbsolute), p.Url); - Assert.Equal(new Uri("/foo/bar", UriKind.RelativeOrAbsolute), p.UrlPath); - - Assert.NotNull(p.Friends); - Assert.Equal(10, p.Friends.Count); - - Assert.NotNull(p.BestFriend); - Assert.Equal("The Fonz", p.BestFriend.Name); - Assert.Equal(1952, p.BestFriend.Since); - } - - [Fact] - public void Can_Deserialize_Elements_With_Namespace_Autodetect_Namespace() { - var doc = CreateElementsXml(); - var response = new RestResponse { Content = doc }; - - var d = new XmlDeserializer(); - var p = d.Deserialize(response); - - Assert.Equal("John Sheehan", p.Name); - Assert.Equal(new DateTime(2009, 9, 25, 0, 6, 1), p.StartDate); - Assert.Equal(28, p.Age); - Assert.Equal(long.MaxValue, p.BigNumber); - Assert.Equal(99.9999m, p.Percent); - Assert.Equal(false, p.IsCool); - Assert.Equal(new Guid(GuidString), p.UniqueId); - Assert.Equal(new Uri("http://example.com", UriKind.RelativeOrAbsolute), p.Url); - Assert.Equal(new Uri("/foo/bar", UriKind.RelativeOrAbsolute), p.UrlPath); - - Assert.NotNull(p.Friends); - Assert.Equal(10, p.Friends.Count); - - Assert.NotNull(p.BestFriend); - Assert.Equal("The Fonz", p.BestFriend.Name); - Assert.Equal(1952, p.BestFriend.Since); - } - - [Fact] - public void Can_Deserialize_Attributes_With_Namespace() { - var doc = CreateAttributesXml(); - var response = new RestResponse { Content = doc }; - - var d = new XmlDeserializer(); - d.Namespace = "http://restsharp.org"; - var p = d.Deserialize(response); - - Assert.Equal("John Sheehan", p.Name); - Assert.Equal(new DateTime(2009, 9, 25, 0, 6, 1), p.StartDate); - Assert.Equal(28, p.Age); - Assert.Equal(long.MaxValue, p.BigNumber); - Assert.Equal(99.9999m, p.Percent); - Assert.Equal(false, p.IsCool); - Assert.Equal(new Guid(GuidString), p.UniqueId); - Assert.Equal(new Uri("http://example.com", UriKind.RelativeOrAbsolute), p.Url); - Assert.Equal(new Uri("/foo/bar", UriKind.RelativeOrAbsolute), p.UrlPath); - - Assert.NotNull(p.BestFriend); - Assert.Equal("The Fonz", p.BestFriend.Name); - Assert.Equal(1952, p.BestFriend.Since); - } - - [Fact] - public void Ignore_Protected_Property_That_Exists_In_Data() { - var doc = CreateElementsXml(); - var response = new RestResponse { Content = doc }; - - var d = new XmlDeserializer(); - d.Namespace = "http://restsharp.org"; - var p = d.Deserialize(response); - - Assert.Null(p.IgnoreProxy); - } - - [Fact] - public void Ignore_ReadOnly_Property_That_Exists_In_Data() { - var doc = CreateElementsXml(); - var response = new RestResponse { Content = doc }; - - var d = new XmlDeserializer(); - d.Namespace = "http://restsharp.org"; - var p = d.Deserialize(response); - - Assert.Null(p.ReadOnlyProxy); - } - - [Fact] - public void Can_Deserialize_Names_With_Underscores_With_Namespace() { - var doc = CreateUnderscoresXml(); - var response = new RestResponse { Content = doc }; - - var d = new XmlDeserializer(); - d.Namespace = "http://restsharp.org"; - var p = d.Deserialize(response); - - Assert.Equal("John Sheehan", p.Name); - Assert.Equal(new DateTime(2009, 9, 25, 0, 6, 1), p.StartDate); - Assert.Equal(28, p.Age); - Assert.Equal(long.MaxValue, p.BigNumber); - Assert.Equal(99.9999m, p.Percent); - Assert.Equal(false, p.IsCool); - Assert.Equal(new Guid(GuidString), p.UniqueId); - Assert.Equal(new Uri("http://example.com", UriKind.RelativeOrAbsolute), p.Url); - Assert.Equal(new Uri("/foo/bar", UriKind.RelativeOrAbsolute), p.UrlPath); - - Assert.NotNull(p.Friends); - Assert.Equal(10, p.Friends.Count); - - Assert.NotNull(p.BestFriend); - Assert.Equal("The Fonz", p.BestFriend.Name); - Assert.Equal(1952, p.BestFriend.Since); - - Assert.NotNull(p.Foes); - Assert.Equal(5, p.Foes.Count); - Assert.Equal("Yankees", p.Foes.Team); - } - - [Fact] - public void Can_Deserialize_List_Of_Primitives_With_Namespace() { - var doc = CreateListOfPrimitivesXml(); - var response = new RestResponse { Content = doc }; - - var d = new XmlDeserializer(); - d.Namespace = "http://restsharp.org"; - var a = d.Deserialize>(response); - - Assert.Equal(2, a.Count); - Assert.Equal("first", a[0].Value); - Assert.Equal("second", a[1].Value); - } - - private static string CreateListOfPrimitivesXml() { - var doc = new XDocument(); - var ns = XNamespace.Get("http://restsharp.org"); - var root = new XElement(ns + "artists"); - root.Add(new XElement(ns + "artist", "first")); - root.Add(new XElement(ns + "artist", "second")); - doc.Add(root); - return doc.ToString(); - } - - private static string CreateUnderscoresXml() { - var doc = new XDocument(); - var ns = XNamespace.Get("http://restsharp.org"); - var root = new XElement(ns + "Person"); - root.Add(new XElement(ns + "Name", "John Sheehan")); - root.Add(new XElement(ns + "Start_Date", new DateTime(2009, 9, 25, 0, 6, 1))); - root.Add(new XAttribute(ns + "Age", 28)); - root.Add(new XElement(ns + "Percent", 99.9999m)); - root.Add(new XElement(ns + "Big_Number", long.MaxValue)); - root.Add(new XAttribute(ns + "Is_Cool", false)); - root.Add(new XElement(ns + "Ignore", "dummy")); - root.Add(new XAttribute(ns + "Read_Only", "dummy")); - root.Add(new XAttribute(ns + "Unique_Id", new Guid(GuidString))); - root.Add(new XElement(ns + "Url", "http://example.com")); - root.Add(new XElement(ns + "Url_Path", "/foo/bar")); - - root.Add(new XElement(ns + "Best_Friend", - new XElement(ns + "Name", "The Fonz"), - new XAttribute(ns + "Since", 1952) - )); - - var friends = new XElement(ns + "Friends"); - for (int i = 0; i < 10; i++) { - friends.Add(new XElement(ns + "Friend", - new XElement(ns + "Name", "Friend" + i), - new XAttribute(ns + "Since", DateTime.Now.Year - i) - )); - } - root.Add(friends); - - var foes = new XElement(ns + "Foes"); - foes.Add(new XAttribute(ns + "Team", "Yankees")); - for (int i = 0; i < 5; i++) { - foes.Add(new XElement(ns + "Foe", new XElement(ns + "Nickname", "Foe" + i))); - } - root.Add(foes); - - doc.Add(root); - return doc.ToString(); - } - - private static string CreateElementsXml() { - var doc = new XDocument(); - var ns = XNamespace.Get("http://restsharp.org"); - var root = new XElement(ns + "Person"); - root.Add(new XElement(ns + "Name", "John Sheehan")); - root.Add(new XElement(ns + "StartDate", new DateTime(2009, 9, 25, 0, 6, 1))); - root.Add(new XElement(ns + "Age", 28)); - root.Add(new XElement(ns + "Percent", 99.9999m)); - root.Add(new XElement(ns + "BigNumber", long.MaxValue)); - root.Add(new XElement(ns + "IsCool", false)); - root.Add(new XElement(ns + "Ignore", "dummy")); - root.Add(new XElement(ns + "ReadOnly", "dummy")); - root.Add(new XElement(ns + "UniqueId", new Guid(GuidString))); - root.Add(new XElement(ns + "Url", "http://example.com")); - root.Add(new XElement(ns + "UrlPath", "/foo/bar")); - - root.Add(new XElement(ns + "BestFriend", - new XElement(ns + "Name", "The Fonz"), - new XElement(ns + "Since", 1952) - )); - - var friends = new XElement(ns + "Friends"); - for (int i = 0; i < 10; i++) { - friends.Add(new XElement(ns + "Friend", - new XElement(ns + "Name", "Friend" + i), - new XElement(ns + "Since", DateTime.Now.Year - i) - )); - } - root.Add(friends); - - doc.Add(root); - return doc.ToString(); - } - - private static string CreateAttributesXml() { - var doc = new XDocument(); - var ns = XNamespace.Get("http://restsharp.org"); - var root = new XElement(ns + "Person"); - root.Add(new XAttribute(ns + "Name", "John Sheehan")); - root.Add(new XAttribute(ns + "StartDate", new DateTime(2009, 9, 25, 0, 6, 1))); - root.Add(new XAttribute(ns + "Age", 28)); - root.Add(new XAttribute(ns + "Percent", 99.9999m)); - root.Add(new XAttribute(ns + "BigNumber", long.MaxValue)); - root.Add(new XAttribute(ns + "IsCool", false)); - root.Add(new XAttribute(ns + "Ignore", "dummy")); - root.Add(new XAttribute(ns + "ReadOnly", "dummy")); - root.Add(new XAttribute(ns + "UniqueId", new Guid(GuidString))); - root.Add(new XAttribute(ns + "Url", "http://example.com")); - root.Add(new XAttribute(ns + "UrlPath", "/foo/bar")); - - root.Add(new XElement(ns + "BestFriend", - new XAttribute(ns + "Name", "The Fonz"), - new XAttribute(ns + "Since", 1952) - )); - - doc.Add(root); - return doc.ToString(); - } - } -} diff --git a/RestSharp.Tests/NuSpecUpdateTask.cs b/RestSharp.Tests/NuSpecUpdateTask.cs deleted file mode 100644 index 33b3ba1bd..000000000 --- a/RestSharp.Tests/NuSpecUpdateTask.cs +++ /dev/null @@ -1,122 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Xunit; -using System.Reflection; -using System.IO; -using System.Xml.Linq; -using System.Text.RegularExpressions; - -namespace RestSharp.Tests -{ - public class NuSpecUpdateTask - { - public abstract class BaseNuSpecUpdateTest - { - protected string FileName { get; set; } - - protected string ComputedFileName - { - get { return this.FileName.Replace(".nuspec", "-computed.nuspec"); } - } - - protected BaseNuSpecUpdateTest() - { - this.FileName = Path.Combine("SampleData", "restsharp.nuspec"); - this.Setup(); - } - - protected abstract void Setup(); - } - - public class Execute - { - public class WhenSpecFileNotSpecified - { - [Fact] - public void ReturnsFalse() - { - var task = new Build.NuSpecUpdateTask(); - Assert.False(task.Execute()); - } - } - - public class WhenInformationalVersionIsNotDefined : BaseNuSpecUpdateTest - { - protected override void Setup() { } - - [Fact] - public void PullsVersionAttributeInstead() - { - var task = new Build.NuSpecUpdateTask(); - task.SpecFile = this.FileName; - task.SourceAssemblyFile = "RestSharp.Tests.dll"; - task.Execute(); - - Assert.Equal("1.0.0.0", task.Version); - } - } - - public class WhenSpecFileIsValid : BaseNuSpecUpdateTest - { - private Build.NuSpecUpdateTask _subject = new Build.NuSpecUpdateTask(); - private bool _result; - - private string _expectedId = "RestSharp"; - private string _expectedDescription = "Simple REST and HTTP API Client"; - private string _expectedAuthors = "John Sheehan, RestSharp Community"; - private string _expectedOwners = "John Sheehan, RestSharp Community"; - private Regex _expectedVersion = new Regex(@"^\d+\.\d+\.\d+(-\w+)?$", RegexOptions.Compiled); - - protected override void Setup() - { - this._subject.SpecFile = this.FileName; - this._subject.SourceAssemblyFile = "RestSharp.dll"; - this._result = this._subject.Execute(); - } - - [Fact] - public void ReturnsTrue() - { - Assert.True(this._result); - } - - [Fact] - public void PullsIdFromAssembly() - { - Assert.Equal(this._expectedId, this._subject.Id); - } - - [Fact] - public void PullsDescriptionFromAssembly() - { - Assert.Equal(this._expectedDescription, this._subject.Description); - } - - [Fact] - public void PullsVersionFromAssemblyInfo() - { - Assert.True(this._expectedVersion.IsMatch(this._subject.Version)); - } - - [Fact] - public void PullsAuthorsFromAssemblyInfo() - { - Assert.Equal(this._expectedAuthors, this._subject.Authors); - } - - [Fact] - public void UpdatesSpecFile() - { - var doc = XDocument.Load(this.ComputedFileName); - Assert.Equal(this._expectedId, doc.Descendants("id").First().Value); - Assert.Equal(this._expectedDescription, doc.Descendants("description").First().Value); - Assert.Equal(this._expectedAuthors, doc.Descendants("authors").First().Value); - Assert.Equal(this._expectedOwners, doc.Descendants("owners").First().Value); - Assert.True(this._expectedVersion.IsMatch(doc.Descendants("version").First().Value)); - } - } - } - } -} diff --git a/RestSharp.Tests/Properties/AssemblyInfo.cs b/RestSharp.Tests/Properties/AssemblyInfo.cs deleted file mode 100644 index 0dcc7f28d..000000000 --- a/RestSharp.Tests/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("RestSharp.Tests")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("Microsoft")] -[assembly: AssemblyProduct("RestSharp.Tests")] -[assembly: AssemblyCopyright("Copyright © Microsoft 2009")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("82c9d878-3d67-40fe-ac6b-6842605a04be")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/RestSharp.Tests/RestRequestTests.cs b/RestSharp.Tests/RestRequestTests.cs deleted file mode 100644 index 2e9342dfe..000000000 --- a/RestSharp.Tests/RestRequestTests.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Xunit; -using System.Globalization; - -namespace RestSharp.Tests { - public class RestRequestTests { - public RestRequestTests() { - System.Threading.Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture; - System.Threading.Thread.CurrentThread.CurrentUICulture = CultureInfo.InstalledUICulture; - } - - [Fact] - public void Can_Add_Object_With_IntegerArray_property() { - var request = new RestRequest(); - request.AddObject(new { Items = new int[] { 2, 3, 4 } }); - } - } -} diff --git a/RestSharp.Tests/RestSharp.Tests.csproj b/RestSharp.Tests/RestSharp.Tests.csproj deleted file mode 100644 index 7e011bd26..000000000 --- a/RestSharp.Tests/RestSharp.Tests.csproj +++ /dev/null @@ -1,228 +0,0 @@ - - - - Debug - AnyCPU - 9.0.30729 - 2.0 - {1464E4AC-18BB-4F23-8A0B-68196F9E1871} - Library - Properties - RestSharp.Tests - RestSharp.Tests - v3.5 - 512 - - - 3.5 - - publish\ - true - Disk - false - Foreground - 7 - Days - false - false - true - 0 - 1.0.0.%2a - false - false - true - ..\ - true - - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - AllRules.ruleset - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - AllRules.ruleset - - - - - - - - - - - - - - - False - ..\packages\xunit.1.9.2\lib\net20\xunit.dll - - - False - ..\packages\xunit.extensions.1.9.2\lib\net20\xunit.extensions.dll - - - False - ..\packages\xunit.1.9.0.1566\lib\xunit.runner.tdnet.dll - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - PreserveNewest - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - - - False - .NET Framework 3.5 SP1 Client Profile - false - - - False - .NET Framework 3.5 SP1 - true - - - False - Windows Installer 3.1 - true - - - - - - Always - - - - - - - {CCC30138-3D68-44D8-AF1A-D22F769EE8DC} - RestSharp.Build - - - {2ECECFBF-5F3E-40EE-A963-72336DC7ABE2} - RestSharp - - - - \ No newline at end of file diff --git a/RestSharp.Tests/SampleClasses/BooleanTest.cs b/RestSharp.Tests/SampleClasses/BooleanTest.cs deleted file mode 100644 index 33f9ab294..000000000 --- a/RestSharp.Tests/SampleClasses/BooleanTest.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace RestSharp.Tests.SampleClasses -{ - public class BooleanTest - { - public bool Value { get; set; } - } -} diff --git a/RestSharp.Tests/SampleClasses/EmployeeTracker.cs b/RestSharp.Tests/SampleClasses/EmployeeTracker.cs deleted file mode 100644 index 42c81b614..000000000 --- a/RestSharp.Tests/SampleClasses/EmployeeTracker.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace RestSharp.Tests.SampleClasses -{ - public class EmployeeTracker - { - /// - /// Key: Employee name. - /// Value: Messages sent to employee. - /// - public Dictionary> EmployeesMail { get; set; } - /// - /// Key: Employee name. - /// Value: Hours worked this each week. - /// - public Dictionary>> EmployeesTime { get; set; } - - /// - /// Key: Employee name. - /// Value: Payments made to employee - /// - public Dictionary> EmployeesPay { get; set; } - } - - public class Payment - { - public PaymentType Type { get; set; } - public Int32 Amount { get; set; } - } - - public enum PaymentType - { - Bonus, - Monthly, - BiWeekly - } -} diff --git a/RestSharp.Tests/SampleClasses/EnumTest.cs b/RestSharp.Tests/SampleClasses/EnumTest.cs deleted file mode 100644 index 5017c894e..000000000 --- a/RestSharp.Tests/SampleClasses/EnumTest.cs +++ /dev/null @@ -1,62 +0,0 @@ -namespace RestSharp.Tests.SampleClasses -{ - public enum ByteEnum : byte - { - EnumMin = 0, - EnumMax = 255 - } - - public enum SByteEnum : sbyte - { - EnumMin = -128, - EnumMax = 127 - } - - public enum ShortEnum : short - { - EnumMin = -32768, - EnumMax = 32767 - } - - public enum UShortEnum : ushort - { - EnumMin = 0, - EnumMax = 65535 - } - - public enum IntEnum : int - { - EnumMin = -2147483648, - EnumMax = 2147483647 - } - - public enum UIntEnum : uint - { - EnumMin = 0, - EnumMax = 4294967295 - } - - public enum LongEnum : long - { - EnumMin = -9223372036854775808, - EnumMax = 9223372036854775807 - } - - public enum ULongEnum : ulong - { - EnumMin = 0, - EnumMax = 18446744073709551615 - } - - public class JsonEnumTypesTestStructure - { - public ByteEnum ByteEnumType { get; set; } - public SByteEnum SByteEnumType { get; set; } - public ShortEnum ShortEnumType { get; set; } - public UShortEnum UShortEnumType { get; set; } - public IntEnum IntEnumType { get; set; } - public UIntEnum UIntEnumType { get; set; } - public LongEnum LongEnumType { get; set; } - public ULongEnum ULongEnumType { get; set; } - } -} \ No newline at end of file diff --git a/RestSharp.Tests/SampleClasses/GoogleWeatherWithAttributes.cs b/RestSharp.Tests/SampleClasses/GoogleWeatherWithAttributes.cs deleted file mode 100644 index 1b0fc889b..000000000 --- a/RestSharp.Tests/SampleClasses/GoogleWeatherWithAttributes.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System.Collections.Generic; -using RestSharp.Deserializers; - -namespace RestSharp.Tests.SampleClasses -{ - public class GoogleWeatherApi - { - public string Version { get; set; } - public GoogleWeather Weather { get; set; } - } - - public class GoogleWeather : List - { - public string ModuleId { get; set; } - public string TabId { get; set; } - public string MobileRow { get; set; } - public string MobileZipped { get; set; } - public string Row { get; set; } - public string Section { get; set; } - - [DeserializeAs(Name = "forecast_information")] - public ForecastInformation Forecast { get; set; } - - [DeserializeAs(Name = "current_conditions")] - public CurrentConditions Current { get; set; } - } - - public class GoogleDataElement - { - public string Data { get; set; } - } - - public class ForecastInformation - { - public GoogleDataElement City { get; set; } - public GoogleDataElement PostalCode { get; set; } - public GoogleDataElement ForecastDate { get; set; } - public GoogleDataElement UnitSystem { get; set; } - } - - public class CurrentConditions - { - public GoogleDataElement Condition { get; set; } - public GoogleDataElement TempC { get; set; } - public GoogleDataElement Humidity { get; set; } - public GoogleDataElement Icon { get; set; } - public GoogleDataElement WindCondition { get; set; } - } - - public class ForecastConditions - { - public GoogleDataElement DayOfWeek { get; set; } - public GoogleDataElement Condition { get; set; } - public GoogleDataElement Low { get; set; } - public GoogleDataElement High { get; set; } - public GoogleDataElement Icon { get; set; } - } - - -} diff --git a/RestSharp.Tests/SampleClasses/JsonLists.cs b/RestSharp.Tests/SampleClasses/JsonLists.cs deleted file mode 100644 index 78bf6ad59..000000000 --- a/RestSharp.Tests/SampleClasses/JsonLists.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace RestSharp.Tests.SampleClasses -{ - public class JsonLists - { - public List Names { get; set; } - public List Numbers { get; set; } - } -} diff --git a/RestSharp.Tests/SampleClasses/Lastfm.cs b/RestSharp.Tests/SampleClasses/Lastfm.cs deleted file mode 100644 index 8b984b39c..000000000 --- a/RestSharp.Tests/SampleClasses/Lastfm.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace RestSharp.Tests.SampleClasses.Lastfm -{ - public class Event : LastfmBase - { - public string id { get; set; } - public string title { get; set; } - public EventArtistList artists { get; set; } - public Venue venue { get; set; } - public DateTime startDate { get; set; } - public string description { get; set; } - public int attendance { get; set; } - public int reviews { get; set; } - public string tag { get; set; } - public string url { get; set; } - public string headliner { get; set; } - public int cancelled { get; set; } - } - - public class EventArtistList : List - { - } - - public class artist - { - public string Value { get; set; } - } - - public abstract class LastfmBase - { - public string status { get; set; } - public Error error { get; set; } - } - - public class Error - { - public string Value { get; set; } - public int code { get; set; } - } -} diff --git a/RestSharp.Tests/SampleClasses/ListSamples.cs b/RestSharp.Tests/SampleClasses/ListSamples.cs deleted file mode 100644 index 64b3d4ec3..000000000 --- a/RestSharp.Tests/SampleClasses/ListSamples.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace RestSharp.Tests.SampleClasses -{ - public class SimpleTypesListSample - { - public List Names { get; set; } - public List Numbers { get; set; } - } - - public class InlineListSample - { - public int Count { get; set; } - public List images { get; set; } - public List Images { get; set; } - } - - public class NestedListSample - { - public List images { get; set; } - public List Images { get; set; } - } - - public class EmptyListSample - { - public List images { get; set; } - public List Images { get; set; } - } - - public class Image - { - public string Src { get; set; } - public string Value { get; set; } - } - - public class image - { - public string Src { get; set; } - public string Value { get; set; } - } -} diff --git a/RestSharp.Tests/SampleClasses/Oddball.cs b/RestSharp.Tests/SampleClasses/Oddball.cs deleted file mode 100644 index 5e7f0238b..000000000 --- a/RestSharp.Tests/SampleClasses/Oddball.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace RestSharp.Tests.SampleClasses -{ - public class Oddball - { - public string Sid { get; set; } - public string FriendlyName { get; set; } - - [Deserializers.DeserializeAs(Name = "oddballPropertyName")] - public string GoodPropertyName { get; set; } - } -} diff --git a/RestSharp.Tests/SampleClasses/SOUser.cs b/RestSharp.Tests/SampleClasses/SOUser.cs deleted file mode 100644 index 5a02a7493..000000000 --- a/RestSharp.Tests/SampleClasses/SOUser.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace RestSharp.Tests.SampleClasses -{ - public class SOUser - { - public int Id { get; set; } - public int Reputation { get; set; } - public long CreationDate { get; set; } - public string DisplayName { get; set; } - public string EmailHash { get; set; } - public string Age { get; set; } - public long LastAccessDate { get; set; } - public string WebsiteUrl { get; set; } - public string Location { get; set; } - public string AboutMe { get; set; } - public int Views { get; set; } - public int UpVotes { get; set; } - public int DownVotes { get; set; } - } -} diff --git a/RestSharp.Tests/SampleClasses/TwilioCallList.cs b/RestSharp.Tests/SampleClasses/TwilioCallList.cs deleted file mode 100644 index d7245fe65..000000000 --- a/RestSharp.Tests/SampleClasses/TwilioCallList.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace RestSharp.Tests.SampleClasses -{ - public class TwilioCallList : List - { - public int Page { get; set; } - public int NumPages { get; set; } - } - - public class Call - { - public string Sid { get; set; } - } -} diff --git a/RestSharp.Tests/SampleClasses/eventful.cs b/RestSharp.Tests/SampleClasses/eventful.cs deleted file mode 100644 index b0b401b62..000000000 --- a/RestSharp.Tests/SampleClasses/eventful.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace RestSharp.Tests.SampleClasses -{ - public class VenueSearch - { - public string total_items { get; set; } - public string page_size { get; set; } - public string page_count { get; set; } - public string page_number { get; set; } - public string page_items { get; set; } - public string first_item { get; set; } - public string last_item { get; set; } - public string search_time { get; set; } - public List venues { get; set; } - } - - public class PerformerSearch - { - public string total_items { get; set; } - public string page_size { get; set; } - public string page_count { get; set; } - public string page_number { get; set; } - public string page_items { get; set; } - public string first_item { get; set; } - public string last_item { get; set; } - public string search_time { get; set; } - public List performers { get; set; } - } - - public class Performer - { - public string id { get; set; } - public string url { get; set; } - public string name { get; set; } - public string short_bio { get; set; } - public DateTime? created { get; set; } - public string creator { get; set; } - public string demand_count { get; set; } - public string demand_member_count { get; set; } - public string event_count { get; set; } - public ServiceImage image { get; set; } - } - - public class Venue - { - public string id { get; set; } - public string url { get; set; } - public string name { get; set; } - public string venue_name { get; set; } - public string description { get; set; } - public string venue_type { get; set; } - public string address { get; set; } - public string city_name { get; set; } - public string region_name { get; set; } - public string postal_code { get; set; } - public string country_name { get; set; } - public string longitude { get; set; } - public string latitude { get; set; } - public string event_count { get; set; } - } - - public class ServiceImage - { - public ServiceImage1 thumb { get; set; } - public ServiceImage1 small { get; set; } - public ServiceImage1 medium { get; set; } - } - - public class ServiceImage1 - { - public string url { get; set; } - public string width { get; set; } - public string height { get; set; } - } - - public class Event - { - public string id { get; set; } - public string url { get; set; } - public string title { get; set; } - public string description { get; set; } - public string start_time { get; set; } - public string venue_name { get; set; } - public string venue_id { get; set; } - public List performers { get; set; } - } -} \ No newline at end of file diff --git a/RestSharp.Tests/SampleClasses/foursq.cs b/RestSharp.Tests/SampleClasses/foursq.cs deleted file mode 100644 index f346661d1..000000000 --- a/RestSharp.Tests/SampleClasses/foursq.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace RestSharp.Tests.SampleClasses -{ - public class VenuesResponse - { - public List Groups { get; set; } - } - - public class Group - { - public string Name { get; set; } - } -} diff --git a/RestSharp.Tests/SampleClasses/googleweather.cs b/RestSharp.Tests/SampleClasses/googleweather.cs deleted file mode 100644 index 373244f0b..000000000 --- a/RestSharp.Tests/SampleClasses/googleweather.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace RestSharp.Tests.SampleClasses -{ - public class xml_api_reply - { - public string version { get; set; } - public Weather weather { get; set; } - } - - public class Weather : List - { - public string module_id { get; set; } - public string tab_id { get; set; } - public string mobile_row { get; set; } - public string mobile_zipped { get; set; } - public string row { get; set; } - public string section { get; set; } - public Forecast_information forecast_information { get; set; } - public Current_conditions current_conditions { get; set; } - //public T forecast_conditions { get; set; } - } - - public class DataElement - { - public string data { get; set; } - } - - public class Forecast_information - { - public DataElement city { get; set; } - public DataElement postal_code { get; set; } - public DataElement forecast_date { get; set; } - public DataElement unit_system { get; set; } - } - - public class Current_conditions - { - public DataElement condition { get; set; } - public DataElement temp_c { get; set; } - public DataElement humidity { get; set; } - public DataElement icon { get; set; } - public DataElement wind_condition { get; set; } - } - - public class forecast_conditions - { - public DataElement day_of_week { get; set; } - public DataElement condition { get; set; } - public DataElement low { get; set; } - public DataElement high { get; set; } - public DataElement icon { get; set; } - } -} diff --git a/RestSharp.Tests/SampleClasses/misc.cs b/RestSharp.Tests/SampleClasses/misc.cs deleted file mode 100644 index ed5d029e6..000000000 --- a/RestSharp.Tests/SampleClasses/misc.cs +++ /dev/null @@ -1,207 +0,0 @@ -#region Licensed -// Copyright 2010 John Sheehan -// -// 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. -#endregion - -using System; -using System.Collections.Generic; -using RestSharp.Serializers; - -namespace RestSharp.Tests -{ - public class PersonForXml - { - public string Name { get; set; } - public DateTime StartDate { get; set; } - public int Age { get; set; } - public decimal Percent { get; set; } - public long BigNumber { get; set; } - public bool IsCool { get; set; } - public List Friends { get; set; } - public Friend BestFriend { get; set; } - - protected string Ignore { get; set; } - public string IgnoreProxy { get { return Ignore; } } - - protected string ReadOnly { get { return null; } } - public string ReadOnlyProxy { get { return ReadOnly; } } - - public FoeList Foes { get; set; } - - public Guid UniqueId { get; set; } - public Guid EmptyGuid { get; set; } - - public Uri Url { get; set; } - public Uri UrlPath { get; set; } - - public Order Order { get; set; } - - public Disposition Disposition { get; set; } - - } - - public class IncomingInvoice - { - public int ConceptId { get; set; } - } - - public class PersonForJson - { - public string Name { get; set; } - public DateTime StartDate { get; set; } - public int Age { get; set; } - public decimal Percent { get; set; } - public long BigNumber { get; set; } - public bool IsCool { get; set; } - public List Friends { get; set; } - public Friend BestFriend { get; set; } - public Guid Guid { get; set; } - public Guid EmptyGuid { get; set; } - public Uri Url { get; set; } - public Uri UrlPath { get; set; } - - protected string Ignore { get; set; } - public string IgnoreProxy { get { return Ignore; } } - - protected string ReadOnly { get { return null; } } - public string ReadOnlyProxy { get { return ReadOnly; } } - - public Dictionary Foes { get; set; } - - public Order Order { get; set; } - - public Disposition Disposition { get; set; } - } - - public enum Order - { - First, - Second, - Third - } - - public enum Disposition - { - Friendly, - SoSo, - SteerVeryClear - } - - public class Friend - { - public string Name { get; set; } - public int Since { get; set; } - } - - public class Foe - { - public string Nickname { get; set; } - } - - public class FoeList : List - { - public string Team { get; set; } - } - - public class Birthdate - { - public DateTime Value { get; set; } - } - - public class OrderedProperties - { - [SerializeAs(Index = 2)] - public string Name { get; set; } - [SerializeAs(Index = 3)] - public int Age { get; set; } - [SerializeAs(Index = 1)] - public DateTime StartDate { get; set; } - } - - public class ObjectProperties - { - public object ObjectProperty { get; set; } - } - - public class DatabaseCollection : List - { - } - - public class Database - { - public string Name { get; set; } - public string InitialCatalog { get; set; } - public string DataSource { get; set; } - } - - public class Generic - { - public T Data { get; set; } - } - - public class GenericWithList - { - public List Items { get; set; } - } - - public class GuidList - { - public List Ids { get; set; } - } - - public class DateTimeTestStructure - { - public DateTime DateTime { get; set; } - public DateTime? NullableDateTimeWithNull { get; set; } - public DateTime? NullableDateTimeWithValue { get; set; } - public DateTimeOffset DateTimeOffset { get; set; } - public DateTimeOffset? NullableDateTimeOffsetWithNull { get; set; } - public DateTimeOffset? NullableDateTimeOffsetWithValue { get; set; } - } - - public class Iso8601DateTimeTestStructure - { - public DateTime DateTimeLocal { get; set; } - public DateTime DateTimeUtc { get; set; } - public DateTime DateTimeWithOffset { get; set; } - } - - public class TimeSpanTestStructure - { - public TimeSpan Tick { get; set; } - public TimeSpan Millisecond { get; set; } - public TimeSpan Second { get; set; } - public TimeSpan Minute { get; set; } - public TimeSpan Hour { get; set; } - public TimeSpan? NullableWithoutValue { get; set; } - public TimeSpan? NullableWithValue { get; set; } - } - - public class JsonEnumsTestStructure - { - public Disposition Upper { get; set; } - public Disposition Lower { get; set; } - public Disposition CamelCased { get; set; } - public Disposition Underscores { get; set; } - public Disposition LowerUnderscores { get; set; } - public Disposition Dashes { get; set; } - public Disposition LowerDashes { get; set; } - public Disposition Integer { get; set; } - } - - public class DecimalNumber - { - public decimal Value { get; set; } - } -} diff --git a/RestSharp.Tests/SampleClasses/nullables.cs b/RestSharp.Tests/SampleClasses/nullables.cs deleted file mode 100644 index 467d6516e..000000000 --- a/RestSharp.Tests/SampleClasses/nullables.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace RestSharp.Tests -{ - public class NullableValues - { - public int? Id { get; set; } - public DateTime? StartDate { get; set; } - public Guid? UniqueId { get; set; } - } -} diff --git a/RestSharp.Tests/SampleClasses/twitter.cs b/RestSharp.Tests/SampleClasses/twitter.cs deleted file mode 100644 index ed997fc6c..000000000 --- a/RestSharp.Tests/SampleClasses/twitter.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using RestSharp.Deserializers; - -namespace RestSharp.Tests.SampleClasses -{ - public class status - { - public bool truncated { get; set; } - public string created_at { get; set; } - public string source { get; set; } - public bool favorited { get; set; } - public string in_reply_to_user_id { get; set; } - public string in_reply_to_status_id { get; set; } - public string in_reply_to_screen_name { get; set; } - // ignore contributors for now - public user user { get; set; } - // ignore geo - public long id { get; set; } - public string text { get; set; } - } - - public class user - { - public string url { get; set; } - public string description { get; set; } - public string profile_text_color { get; set; } - public int followers_count { get; set; } - public int statuses_count { get; set; } - public bool geo_enabled { get; set; } - public string profile_background_image_url { get; set; } - public bool notifications { get; set; } - public string created_at { get; set; } - public int friends_count { get; set; } - public string profile_link_color { get; set; } - public bool contributors_enabled { get; set; } - public bool profile_background_tile { get; set; } - public int favourites_count { get; set; } - public string profile_background_color { get; set; } - public string profile_image_url { get; set; } - public string lang { get; set; } - public bool verified { get; set; } - public string profile_sidebar_fill_color { get; set; } - public bool @protected { get; set; } - public string screen_name { get; set; } - public bool following { get; set; } - public string location { get; set; } - public string name { get; set; } - public string time_zone { get; set; } - public string profile_sidebar_border_color { get; set; } - public long id { get; set; } - public int utc_offset { get; set; } - - } - - public class StatusList : List - { - } - - public class complexStatus - { - public bool truncated { get; set; } - public string created_at { get; set; } - public string source { get; set; } - public bool favorited { get; set; } - public string in_reply_to_user_id { get; set; } - public string in_reply_to_status_id { get; set; } - public string in_reply_to_screen_name { get; set; } - // ignore contributors for now - [DeserializeAs(Name="user.following")] - public bool follow { get; set; } - // ignore geo - public long id { get; set; } - public string text { get; set; } - } - - public class StatusComplexList : List - { - } -} diff --git a/RestSharp.Tests/SampleData/GenericWithList.txt b/RestSharp.Tests/SampleData/GenericWithList.txt deleted file mode 100644 index a243a842f..000000000 --- a/RestSharp.Tests/SampleData/GenericWithList.txt +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Data": - { - "Items": - [ - {"Nickname":"Foe sho"} - ] - } -} \ No newline at end of file diff --git a/RestSharp.Tests/SampleData/InlineListSample.xml b/RestSharp.Tests/SampleData/InlineListSample.xml deleted file mode 100644 index 0cce8dc9c..000000000 --- a/RestSharp.Tests/SampleData/InlineListSample.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - 4 - value1 - value2 - value3 - value4 - \ No newline at end of file diff --git a/RestSharp.Tests/SampleData/Lastfm.xml b/RestSharp.Tests/SampleData/Lastfm.xml deleted file mode 100644 index cbc77d3ec..000000000 --- a/RestSharp.Tests/SampleData/Lastfm.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - 328799 - Philip Glass - - Philip Glass - Orchestra and Chorus of Erfurt Theatre - Philip Glass - - - 8777860 - Barbican Centre - - London - United Kingdom - Silk Street - EC2Y 8DS - - 51.520099 - -0.093609 - - - http://www.last.fm/venue/8777860+Barbican+Centre - http://www.barbican.org.uk - - http://userserve-ak.last.fm/serve/34/418510.jpg - http://userserve-ak.last.fm/serve/64/418510.jpg - http://userserve-ak.last.fm/serve/126/418510.jpg - http://userserve-ak.last.fm/serve/252/418510.jpg - http://userserve-ak.last.fm/serve/500/418510/Barbican+Centre.jpg - - Thu, 12 Jun 2008 19:30:00 - - http://userserve-ak.last.fm/serve/34/39466081.png - http://userserve-ak.last.fm/serve/64/39466081.png - http://userserve-ak.last.fm/serve/126/39466081.png - http://userserve-ak.last.fm/serve/252/39466081.png - 48 - 0 - lastfm:event=328799 - http://www.last.fm/event/328799+Philip+Glass+at+Barbican+Centre+on+12+June+2008 - - - 0 - - \ No newline at end of file diff --git a/RestSharp.Tests/SampleData/ListWithAttributes.xml b/RestSharp.Tests/SampleData/ListWithAttributes.xml deleted file mode 100644 index 57ac54969..000000000 --- a/RestSharp.Tests/SampleData/ListWithAttributes.xml +++ /dev/null @@ -1,55 +0,0 @@ - - - - - CA92d4405c9237c4ea04b56cbda88e128c - Fri, 13 Aug 2010 01:16:22 +0000 - Fri, 13 Aug 2010 01:16:22 +0000 - - AC5ef877a5fe4238be081ea6f3c44186f3 - +15304551166 - +15105555555 - PNe2d8e63b37f46f2adb16f228afdb9058 - queued - Thu, 12 Aug 2010 01:37:05 +0000 - Thu, 12 Aug 2010 01:37:40 +0000 - - - outbound-api - - 2010-04-01 - - - /2010-04-01/Accounts/AC5ef877a5fe4238be081ea6f3c44186f3/Calls/CA92d4405c9237c4ea04b56cbda88e128c - - /2010-04-01/Accounts/AC5ef877a5fe4238be081ea6f3c44186f3/Calls/CA92d4405c9237c4ea04b56cbda88e128c/Notifications - /2010-04-01/Accounts/AC5ef877a5fe4238be081ea6f3c44186f3/Calls/CA92d4405c9237c4ea04b56cbda88e128c/Recordings - - - - CA12345 - Fri, 13 Aug 2010 01:16:22 +0000 - Fri, 13 Aug 2010 01:16:22 +0000 - - AC5ef877a5fe4238be081ea6f3c44186f3 - +15304551166 - +15105555555 - PNe2d8e63b37f46f2adb16f228afdb9058 - queued - Thu, 12 Aug 2010 01:37:05 +0000 - Thu, 12 Aug 2010 01:37:40 +0000 - - - outbound-api - - 2010-04-01 - - - /2010-04-01/Accounts/AC5ef877a5fe4238be081ea6f3c44186f3/Calls/CA92d4405c9237c4ea04b56cbda88e128c - - /2010-04-01/Accounts/AC5ef877a5fe4238be081ea6f3c44186f3/Calls/CA92d4405c9237c4ea04b56cbda88e128c/Notifications - /2010-04-01/Accounts/AC5ef877a5fe4238be081ea6f3c44186f3/Calls/CA92d4405c9237c4ea04b56cbda88e128c/Recordings - - - - \ No newline at end of file diff --git a/RestSharp.Tests/SampleData/NestedListSample.xml b/RestSharp.Tests/SampleData/NestedListSample.xml deleted file mode 100644 index 606914977..000000000 --- a/RestSharp.Tests/SampleData/NestedListSample.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - value1 - value2 - value3 - value4 - - \ No newline at end of file diff --git a/RestSharp.Tests/SampleData/directlists.xml b/RestSharp.Tests/SampleData/directlists.xml deleted file mode 100644 index 9bc1ccb9f..000000000 --- a/RestSharp.Tests/SampleData/directlists.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - sv - lusrvsql3 - Pomtest - Pomtest - - - - - - sv - lusrvsql3 - testdb - Test - - - - - \ No newline at end of file diff --git a/RestSharp.Tests/SampleData/eventful.xml b/RestSharp.Tests/SampleData/eventful.xml deleted file mode 100644 index e0641f8f0..000000000 --- a/RestSharp.Tests/SampleData/eventful.xml +++ /dev/null @@ -1,111 +0,0 @@ - - - 3 - 10 - 1 - 1 - 3 - 1 - 3 - 0.262 - - - http://eventful.com/venues/tivoli-/V0-001-002011236-6 - Tivoli - Tivoli - - -
291 Dandenong Rd
- Prahran - Victoria - VIC - 3181 - Australia - AU - AUS - 145.004726 - -37.859123 - EVDB Geocoder - evdb - - - 0 - 0 - 0 - 0 - -
- - http://eventful.com/brisbane/venues/tivoli-/V0-001-002169294-8 - Tivoli - Tivoli - <br> <br>(No phone for this venue yet.)<br> <br>(No website for this venue yet.)<br> <br> - -
Brisbane
- Brisbane - Queensland - QLD - - Australia - AU - AUS - 153.017 - -27.5 - City Based GeoCodes - evdb - - - 1 - 0 - 0 - 1 - -
- - http://eventful.com/brisbane/venues/the-tivoli-/V0-001-000266914-3 - The Tivoli - The Tivoli - Built in 1917 and restored in Art Deco style, this elegant, fully licensed theatre has state-of-the-art technical features, top-of-the-range facilities, in-house catering by our 5 star chef and dedicated staff available to meet all your needs. - Concert Hall -
52 Costin Street Fortitude Valley
- Brisbane - Queensland - QLD - 4006 - Australia - AU - AUS - 153.031548 - -27.452335 - EVDB Geocoder - evdb - - - 28 - 0 - 7 - 1 - - http://images.evdb.com/images/small/I0-001/001/414/278-2.jpeg - 48 - 48 - - - http://images.evdb.com/images/thumb/I0-001/001/414/278-2.jpeg - 48 - 48 - - - http://images.evdb.com/images/small/I0-001/001/414/278-2.jpeg - 48 - 48 - - - http://images.evdb.com/images/medium/I0-001/001/414/278-2.jpeg - 128 - 128 - - -
-
-
\ No newline at end of file diff --git a/RestSharp.Tests/SampleData/jsondictionary.txt b/RestSharp.Tests/SampleData/jsondictionary.txt deleted file mode 100644 index 5c4ebb76c..000000000 --- a/RestSharp.Tests/SampleData/jsondictionary.txt +++ /dev/null @@ -1,21 +0,0 @@ -{ - "EmployeesPay" : - { - "John": [{"Type":"BiWeekly","Amount":3000},{"Type":"Bonus","Amount":5000}], - "David": [{"Type":"Monthly","Amount":5000},{"Type":"Bonus","Amount":2500}], - "Mary": [{"Type":"BiWeekly","Amount":2000}] - }, - "EmployeesMail" : - { - "John": ["Welcome to Restsharp", "Meetings at 4pm", "Meeting Cancled"], - "David": ["Project deadline is Monday", "Good work"], - "Mary": ["Is there any documentation on Product A", "I'm leaving early today"] - }, - - "EmployeesTime" : - { - "John": [[8, 7, 8, 8, 8], [1, 2, 3]], - "David": [[4, 12, 6, 4],[4, 12, 6, 4]], - "Mary": [[]] - } -} \ No newline at end of file diff --git a/RestSharp.Tests/SampleData/jsonlists.txt b/RestSharp.Tests/SampleData/jsonlists.txt deleted file mode 100644 index 014e3a2a6..000000000 --- a/RestSharp.Tests/SampleData/jsonlists.txt +++ /dev/null @@ -1,17 +0,0 @@ -{ - "names" : [ - "John", - "Bob", - "Albert", - "Jeff", - "Charlie" - ], - "numbers" : [ - 1, - 2, - 3, - 5, - 7, - 11 - ] -} \ No newline at end of file diff --git a/RestSharp.Tests/SampleData/restsharp.nuspec b/RestSharp.Tests/SampleData/restsharp.nuspec deleted file mode 100644 index 51839acdb..000000000 --- a/RestSharp.Tests/SampleData/restsharp.nuspec +++ /dev/null @@ -1,18 +0,0 @@ - - - - $id$ - $version$ - $author$ - $author$ - $description$ - en-US - http://restsharp.org - https://github.com/restsharp/RestSharp/blob/master/LICENSE.txt - http://dl.dropbox.com/u/1827/restsharp100.png - REST HTTP API JSON XML - - - \ No newline at end of file diff --git a/RestSharp.Tests/SampleData/timespans.txt b/RestSharp.Tests/SampleData/timespans.txt deleted file mode 100644 index d5e4baa82..000000000 --- a/RestSharp.Tests/SampleData/timespans.txt +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Tick": "00:00:00.0468006", - "Millisecond": "00:00:00.1250000", - "Second": "00:00:08.0000000", - "Minute": "00:55:02.0000000", - "Hour": "21:30:07.0000000", - "NullableWithoutValue": null, - "NullableWithValue": "21:30:07.0000000", -} \ No newline at end of file diff --git a/RestSharp.Tests/SampleData/underscore_prefix.txt b/RestSharp.Tests/SampleData/underscore_prefix.txt deleted file mode 100644 index 9e895fed5..000000000 --- a/RestSharp.Tests/SampleData/underscore_prefix.txt +++ /dev/null @@ -1,18 +0,0 @@ -{ - "User":{ - "_id":1786, - "_displayName":"John Sheehan", - "Reputation":18332, - "CreationDate":1219071163, - "_displayName":"John Sheehan", - "EmailHash":"lkdsafadfjsadfkjlsdjflkjdsf", - "Age":"28", - "LastAccessDate":1269715453, - "WebsiteUrl":"http://john-sheehan.com/blog", - "Location":"Minnesota", - "AboutMe":"

Follow me on Twitter

\r\n\r\n

Read my blog

\r\n\r\n

Visit managedassembly.com - A community for .NET developers.

\r\n\r\n

I am the founder of RIM Systems, maker of SnapLeague, a web-based league management system for recreational athletics.

", - "Views":1639, - "UpVotes":1665, - "DownVotes":427 - } -} \ No newline at end of file diff --git a/RestSharp.Tests/SampleData/xmllists.xml b/RestSharp.Tests/SampleData/xmllists.xml deleted file mode 100644 index 5136cdc09..000000000 --- a/RestSharp.Tests/SampleData/xmllists.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - John - Bob - Albert - Jeff - Charlie - - - 1 - 2 - 3 - 5 - 7 - 11 - - \ No newline at end of file diff --git a/RestSharp.Tests/SerializerTests.cs b/RestSharp.Tests/SerializerTests.cs deleted file mode 100644 index 2259c0af0..000000000 --- a/RestSharp.Tests/SerializerTests.cs +++ /dev/null @@ -1,444 +0,0 @@ -#region License -// Copyright 2010 John Sheehan -// -// 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. -#endregion - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Xml.Linq; -using RestSharp.Serializers; -using RestSharp.Tests.SampleClasses; -using Xunit; - -namespace RestSharp.Tests -{ - public class SerializerTests - { - public SerializerTests() - { - System.Threading.Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture; - System.Threading.Thread.CurrentThread.CurrentUICulture = CultureInfo.InstalledUICulture; - } - - [Fact] - public void Serializes_Properties_In_Specified_Order() { - var ordered = new OrderedProperties(); - ordered.Name = "Name"; - ordered.Age = 99; - ordered.StartDate = new DateTime(2010, 1, 1); - - var xml = new XmlSerializer(); - var doc = xml.Serialize(ordered); - - var expected = GetSortedPropsXDoc(); - - Assert.Equal(expected.ToString(), doc.ToString()); - } - - [Fact] - public void Can_serialize_simple_POCO() { - var poco = new Person { - Name = "Foo", - Age = 50, - Price = 19.95m, - StartDate = new DateTime(2009, 12, 18, 10, 2, 23), - Items = new List { - new Item { Name = "One", Value = 1 }, - new Item { Name = "Two", Value = 2 }, - new Item { Name = "Three", Value = 3 } - } - }; - - var xml = new XmlSerializer(); - var doc = xml.Serialize(poco); - var expected = GetSimplePocoXDoc(); - - Assert.Equal(expected.ToString(), doc.ToString()); - } - - [Fact] - public void Can_serialize_Enum() - { - var enumClass = new ClassWithEnum - { - Color = Color.Red - }; - - var xml = new XmlSerializer(); - var doc = xml.Serialize(enumClass); - - var expected = new XDocument(); - var root = new XElement("ClassWithEnum"); - root.Add( new XElement("Color", "Red") ); - expected.Add(root); - - Assert.Equal( expected.ToString(), doc.ToString() ); - } - - [Fact] - public void Can_serialize_simple_POCO_With_DateFormat_Specified() { - var poco = new Person { - Name = "Foo", - Age = 50, - Price = 19.95m, - StartDate = new DateTime(2009, 12, 18, 10, 2, 23) - }; - - var xml = new XmlSerializer(); - xml.DateFormat = DateFormat.Iso8601; - var doc = xml.Serialize(poco); - var expected = GetSimplePocoXDocWithIsoDate(); - - Assert.Equal(expected.ToString(), doc.ToString()); - } - - [Fact] - public void Can_serialize_simple_POCO_With_XmlFormat_Specified() - { - var poco = new Person - { - Name = "Foo", - Age = 50, - Price = 19.95m, - StartDate = new DateTime(2009, 12, 18, 10, 2, 23), - IsCool = false - }; - - var xml = new XmlSerializer(); - xml.DateFormat = DateFormat.Iso8601; - var doc = xml.Serialize(poco); - var expected = GetSimplePocoXDocWithXmlProperty(); - - Assert.Equal(expected.ToString(), doc); - } - - [Fact] - public void Can_serialize_simple_POCO_With_Different_Root_Element() { - var poco = new Person { - Name = "Foo", - Age = 50, - Price = 19.95m, - StartDate = new DateTime(2009, 12, 18, 10, 2, 23) - }; - - var xml = new XmlSerializer(); - xml.RootElement = "Result"; - var doc = xml.Serialize(poco); - var expected = GetSimplePocoXDocWithRoot(); - - Assert.Equal(expected.ToString(), doc.ToString()); - } - - [Fact] - public void Can_serialize_simple_POCO_With_Attribute_Options_Defined() { - var poco = new WackyPerson { - Name = "Foo", - Age = 50, - Price = 19.95m, - StartDate = new DateTime(2009, 12, 18, 10, 2, 23) - }; - - var xml = new XmlSerializer(); - var doc = xml.Serialize(poco); - var expected = GetSimplePocoXDocWackyNames(); - - Assert.Equal(expected.ToString(), doc.ToString()); - } - - [Fact] - public void Can_serialize_simple_POCO_With_Attribute_Options_Defined_And_Property_Containing_IList_Elements() - { - var poco = new WackyPerson - { - Name = "Foo", - Age = 50, - Price = 19.95m, - StartDate = new DateTime(2009, 12, 18, 10, 2, 23), - ContactData = new ContactData - { - EmailAddresses = new List - { - new EmailAddress - { - Address = "test@test.com", - Location = "Work" - } - } - } - }; - - var xml = new XmlSerializer(); - var doc = xml.Serialize(poco); - var expected = GetSimplePocoXDocWackyNamesWithIListProperty(); - - Assert.Equal(expected.ToString(), doc.ToString()); - } - - [Fact] - public void Can_serialize_a_list_which_is_the_root_element() - { - var pocoList = new PersonList - { - new Person - { - Name = "Foo", - Age = 50, - Price = 19.95m, - StartDate = new DateTime(2009, 12, 18, 10, 2, 23), - Items = new List - { - new Item {Name = "One", Value = 1}, - new Item {Name = "Two", Value = 2}, - new Item {Name = "Three", Value = 3} - } - }, - new Person - { - Name = "Bar", - Age = 23, - Price = 23.23m, - StartDate = new DateTime(2009, 12, 23, 10, 23, 23), - Items = new List - { - new Item {Name = "One", Value = 1}, - new Item {Name = "Two", Value = 2}, - new Item {Name = "Three", Value = 3} - } - } - }; - - var xml = new XmlSerializer(); - var doc = xml.Serialize(pocoList); - var expected = GetPeopleXDoc(CultureInfo.InvariantCulture); - - Assert.Equal(expected.ToString(), doc); - } - - private class Person - { - public string Name { get; set; } - public int Age { get; set; } - public decimal Price { get; set; } - public DateTime StartDate { get; set; } - public List Items { get; set; } - public bool? IsCool {get;set;} - } - - private class Item - { - public string Name { get; set; } - public int Value { get; set; } - } - - private enum Color - { - Red, - Blue, - Green - } - - private class ClassWithEnum - { - public Color Color { get; set; } - } - - [SerializeAs(Name = "Person")] - private class WackyPerson - { - [SerializeAs(Name = "WackyName", Attribute = true)] - public string Name { get; set; } - - public int Age { get; set; } - - [SerializeAs(Attribute = true)] - public decimal Price { get; set; } - - [SerializeAs(Name = "start_date", Attribute = true)] - public DateTime StartDate { get; set; } - - [SerializeAs(Name = "contact-data")] - public ContactData ContactData { get; set; } - } - - [SerializeAs(Name = "People")] - private class PersonList : List - { - - } - - private class ContactData - { - public ContactData() - { - EmailAddresses = new List(); - } - - [SerializeAs(Name = "email-addresses")] - public List EmailAddresses { get; set; } - } - - [SerializeAs(Name = "email-address")] - private class EmailAddress - { - [SerializeAs(Name = "address")] - public string Address { get; set; } - - [SerializeAs(Name = "location")] - public string Location { get; set; } - } - - private XDocument GetSimplePocoXDoc() - { - var doc = new XDocument(); - var root = new XElement("Person"); - root.Add(new XElement("Name", "Foo"), - new XElement("Age", 50), - new XElement("Price", 19.95m), - new XElement("StartDate", new DateTime(2009, 12, 18, 10, 2, 23).ToString())); - - var items = new XElement("Items"); - items.Add(new XElement("Item", new XElement("Name", "One"), new XElement("Value", 1))); - items.Add(new XElement("Item", new XElement("Name", "Two"), new XElement("Value", 2))); - items.Add(new XElement("Item", new XElement("Name", "Three"), new XElement("Value", 3))); - root.Add(items); - - doc.Add(root); - - return doc; - } - - private XDocument GetSimplePocoXDocWithIsoDate() { - var doc = new XDocument(); - var root = new XElement("Person"); - root.Add(new XElement("Name", "Foo"), - new XElement("Age", 50), - new XElement("Price", 19.95m), - new XElement("StartDate", new DateTime(2009, 12, 18, 10, 2, 23).ToString("s"))); - - doc.Add(root); - - return doc; - } - - private XDocument GetSimplePocoXDocWithXmlProperty() - { - var doc = new XDocument(); - var root = new XElement("Person"); - root.Add(new XElement("Name", "Foo"), - new XElement("Age", 50), - new XElement("Price", 19.95m), - new XElement("StartDate", new DateTime(2009, 12, 18, 10, 2, 23).ToString("s")), - new XElement("IsCool", false)); - - doc.Add(root); - - return doc; - } - - private XDocument GetSimplePocoXDocWithRoot() { - var doc = new XDocument(); - var root = new XElement("Result"); - - var start = new XElement("Person"); - start.Add(new XElement("Name", "Foo"), - new XElement("Age", 50), - new XElement("Price", 19.95m), - new XElement("StartDate", new DateTime(2009, 12, 18, 10, 2, 23).ToString())); - - root.Add(start); - doc.Add(root); - - return doc; - } - - private XDocument GetSimplePocoXDocWackyNames() { - var doc = new XDocument(); - var root = new XElement("Person"); - root.Add(new XAttribute("WackyName", "Foo"), - new XElement("Age", 50), - new XAttribute("Price", 19.95m), - new XAttribute("start_date", new DateTime(2009, 12, 18, 10, 2, 23).ToString())); - - doc.Add(root); - - return doc; - } - - private XDocument GetSimplePocoXDocWackyNamesWithIListProperty() - { - var doc = new XDocument(); - var root = new XElement("Person"); - root.Add(new XAttribute("WackyName", "Foo"), - new XElement("Age", 50), - new XAttribute("Price", 19.95m), - new XAttribute("start_date", new DateTime(2009, 12, 18, 10, 2, 23).ToString()), - new XElement("contact-data", - new XElement("email-addresses", - new XElement("email-address", - new XElement("address", "test@test.com"), - new XElement("location", "Work") - )))); - - doc.Add(root); - - return doc; - } - - private XDocument GetSortedPropsXDoc() { - var doc = new XDocument(); - var root = new XElement("OrderedProperties"); - - root.Add(new XElement("StartDate", new DateTime(2010, 1, 1).ToString())); - root.Add(new XElement("Name", "Name")); - root.Add(new XElement("Age", 99)); - - doc.Add(root); - - return doc; - } - - private XDocument GetPeopleXDoc(CultureInfo culture) - { - var doc = new XDocument(); - var root = new XElement("People"); - var element = new XElement("Person"); - - var items = new XElement("Items"); - items.Add(new XElement("Item", new XElement("Name", "One"), new XElement("Value", 1))); - items.Add(new XElement("Item", new XElement("Name", "Two"), new XElement("Value", 2))); - items.Add(new XElement("Item", new XElement("Name", "Three"), new XElement("Value", 3))); - element.Add(new XElement("Name", "Foo"), - new XElement("Age", 50), - new XElement("Price", 19.95m.ToString(culture)), - new XElement("StartDate", new DateTime(2009, 12, 18, 10, 2, 23).ToString(culture))); - - element.Add(items); - root.Add(element); - element = new XElement("Person"); - - element.Add(new XElement("Name", "Bar"), - new XElement("Age", 23), - new XElement("Price", 23.23m.ToString(culture)), - new XElement("StartDate", new DateTime(2009, 12, 23, 10, 23, 23).ToString(culture))); - - element.Add(items); - - root.Add(element); - doc.Add(root); - - return doc; - } - } -} diff --git a/RestSharp.Tests/SimpleJson.cs b/RestSharp.Tests/SimpleJson.cs deleted file mode 100644 index e34712542..000000000 --- a/RestSharp.Tests/SimpleJson.cs +++ /dev/null @@ -1,2052 +0,0 @@ -//----------------------------------------------------------------------- -// -// Copyright (c) 2011, The Outercurve Foundation. -// -// Licensed under the MIT License (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.opensource.org/licenses/mit-license.php -// -// 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. -// -// Nathan Totten (ntotten.com), Jim Zimmerman (jimzimmerman.com) and Prabir Shrestha (prabir.me) -// https://github.com/facebook-csharp-sdk/simple-json -//----------------------------------------------------------------------- - -// VERSION: 0.26.0 - -// NOTE: uncomment the following line to make SimpleJson class internal. -//#define SIMPLE_JSON_INTERNAL - -// NOTE: uncomment the following line to make JsonArray and JsonObject class internal. -//#define SIMPLE_JSON_OBJARRAYINTERNAL - -// NOTE: uncomment the following line to enable dynamic support. -//#define SIMPLE_JSON_DYNAMIC - -// NOTE: uncomment the following line to enable DataContract support. -//#define SIMPLE_JSON_DATACONTRACT - -// NOTE: uncomment the following line to disable linq expressions/compiled lambda (better performance) instead of method.invoke(). -// define if you are using .net framework <= 3.0 or < WP7.5 -//#define SIMPLE_JSON_NO_LINQ_EXPRESSION - -// NOTE: uncomment the following line if you are compiling under Window Metro style application/library. -// usually already defined in properties -//#define NETFX_CORE; - -// If you are targetting WinStore, WP8 and NET4.5+ PCL make sure to #define SIMPLE_JSON_TYPEINFO; - -// original json parsing code from http://techblog.procurios.nl/k/618/news/view/14605/14863/How-do-I-write-my-own-parser-for-JSON.html - -#if NETFX_CORE -#define SIMPLE_JSON_TYPEINFO -#endif - -using System; -using System.CodeDom.Compiler; -using System.Collections; -using System.Collections.Generic; -#if !SIMPLE_JSON_NO_LINQ_EXPRESSION -using System.Linq.Expressions; -#endif -using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; -#if SIMPLE_JSON_DYNAMIC -using System.Dynamic; -#endif -using System.Globalization; -using System.Reflection; -using System.Runtime.Serialization; -using System.Text; -using RestSharp.Tests.Reflection; - -// ReSharper disable LoopCanBeConvertedToQuery -// ReSharper disable RedundantExplicitArrayCreation -// ReSharper disable SuggestUseVarKeywordEvident -namespace RestSharp.Tests -{ - /// - /// Represents the json array. - /// - [GeneratedCode("simple-json", "1.0.0")] - [EditorBrowsable(EditorBrowsableState.Never)] - [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix")] -#if SIMPLE_JSON_OBJARRAYINTERNAL - internal -#else - public -#endif - class JsonArray : List - { - /// - /// Initializes a new instance of the class. - /// - public JsonArray() { } - - /// - /// Initializes a new instance of the class. - /// - /// The capacity of the json array. - public JsonArray(int capacity) : base(capacity) { } - - /// - /// The json representation of the array. - /// - /// The json representation of the array. - public override string ToString() - { - return SimpleJson.SerializeObject(this) ?? string.Empty; - } - } - - /// - /// Represents the json object. - /// - [GeneratedCode("simple-json", "1.0.0")] - [EditorBrowsable(EditorBrowsableState.Never)] - [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix")] -#if SIMPLE_JSON_OBJARRAYINTERNAL - internal -#else - public -#endif - class JsonObject : -#if SIMPLE_JSON_DYNAMIC - DynamicObject, -#endif - IDictionary - { - /// - /// The internal member dictionary. - /// - private readonly Dictionary _members; - - /// - /// Initializes a new instance of . - /// - public JsonObject() - { - _members = new Dictionary(); - } - - /// - /// Initializes a new instance of . - /// - /// The implementation to use when comparing keys, or null to use the default for the type of the key. - public JsonObject(IEqualityComparer comparer) - { - _members = new Dictionary(comparer); - } - - /// - /// Gets the at the specified index. - /// - /// - public object this[int index] - { - get { return GetAtIndex(_members, index); } - } - - internal static object GetAtIndex(IDictionary obj, int index) - { - if (obj == null) - throw new ArgumentNullException("obj"); - if (index >= obj.Count) - throw new ArgumentOutOfRangeException("index"); - int i = 0; - foreach (KeyValuePair o in obj) - if (i++ == index) return o.Value; - return null; - } - - /// - /// Adds the specified key. - /// - /// The key. - /// The value. - public void Add(string key, object value) - { - _members.Add(key, value); - } - - /// - /// Determines whether the specified key contains key. - /// - /// The key. - /// - /// true if the specified key contains key; otherwise, false. - /// - public bool ContainsKey(string key) - { - return _members.ContainsKey(key); - } - - /// - /// Gets the keys. - /// - /// The keys. - public ICollection Keys - { - get { return _members.Keys; } - } - - /// - /// Removes the specified key. - /// - /// The key. - /// - public bool Remove(string key) - { - return _members.Remove(key); - } - - /// - /// Tries the get value. - /// - /// The key. - /// The value. - /// - public bool TryGetValue(string key, out object value) - { - return _members.TryGetValue(key, out value); - } - - /// - /// Gets the values. - /// - /// The values. - public ICollection Values - { - get { return _members.Values; } - } - - /// - /// Gets or sets the with the specified key. - /// - /// - public object this[string key] - { - get { return _members[key]; } - set { _members[key] = value; } - } - - /// - /// Adds the specified item. - /// - /// The item. - public void Add(KeyValuePair item) - { - _members.Add(item.Key, item.Value); - } - - /// - /// Clears this instance. - /// - public void Clear() - { - _members.Clear(); - } - - /// - /// Determines whether [contains] [the specified item]. - /// - /// The item. - /// - /// true if [contains] [the specified item]; otherwise, false. - /// - public bool Contains(KeyValuePair item) - { - return _members.ContainsKey(item.Key) && _members[item.Key] == item.Value; - } - - /// - /// Copies to. - /// - /// The array. - /// Index of the array. - public void CopyTo(KeyValuePair[] array, int arrayIndex) - { - if (array == null) throw new ArgumentNullException("array"); - int num = Count; - foreach (KeyValuePair kvp in this) - { - array[arrayIndex++] = kvp; - if (--num <= 0) - return; - } - } - - /// - /// Gets the count. - /// - /// The count. - public int Count - { - get { return _members.Count; } - } - - /// - /// Gets a value indicating whether this instance is read only. - /// - /// - /// true if this instance is read only; otherwise, false. - /// - public bool IsReadOnly - { - get { return false; } - } - - /// - /// Removes the specified item. - /// - /// The item. - /// - public bool Remove(KeyValuePair item) - { - return _members.Remove(item.Key); - } - - /// - /// Gets the enumerator. - /// - /// - public IEnumerator> GetEnumerator() - { - return _members.GetEnumerator(); - } - - /// - /// Returns an enumerator that iterates through a collection. - /// - /// - /// An object that can be used to iterate through the collection. - /// - IEnumerator IEnumerable.GetEnumerator() - { - return _members.GetEnumerator(); - } - - /// - /// Returns a json that represents the current . - /// - /// - /// A json that represents the current . - /// - public override string ToString() - { - return SimpleJson.SerializeObject(this); - } - -#if SIMPLE_JSON_DYNAMIC - /// - /// Provides implementation for type conversion operations. Classes derived from the class can override this method to specify dynamic behavior for operations that convert an object from one type to another. - /// - /// Provides information about the conversion operation. The binder.Type property provides the type to which the object must be converted. For example, for the statement (String)sampleObject in C# (CType(sampleObject, Type) in Visual Basic), where sampleObject is an instance of the class derived from the class, binder.Type returns the type. The binder.Explicit property provides information about the kind of conversion that occurs. It returns true for explicit conversion and false for implicit conversion. - /// The result of the type conversion operation. - /// - /// Alwasy returns true. - /// - public override bool TryConvert(ConvertBinder binder, out object result) - { - // - if (binder == null) - throw new ArgumentNullException("binder"); - // - Type targetType = binder.Type; - - if ((targetType == typeof(IEnumerable)) || - (targetType == typeof(IEnumerable>)) || - (targetType == typeof(IDictionary)) || - (targetType == typeof(IDictionary))) - { - result = this; - return true; - } - - return base.TryConvert(binder, out result); - } - - /// - /// Provides the implementation for operations that delete an object member. This method is not intended for use in C# or Visual Basic. - /// - /// Provides information about the deletion. - /// - /// Alwasy returns true. - /// - public override bool TryDeleteMember(DeleteMemberBinder binder) - { - // - if (binder == null) - throw new ArgumentNullException("binder"); - // - return _members.Remove(binder.Name); - } - - /// - /// Provides the implementation for operations that get a value by index. Classes derived from the class can override this method to specify dynamic behavior for indexing operations. - /// - /// Provides information about the operation. - /// The indexes that are used in the operation. For example, for the sampleObject[3] operation in C# (sampleObject(3) in Visual Basic), where sampleObject is derived from the DynamicObject class, is equal to 3. - /// The result of the index operation. - /// - /// Alwasy returns true. - /// - public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result) - { - if (indexes == null) throw new ArgumentNullException("indexes"); - if (indexes.Length == 1) - { - result = ((IDictionary)this)[(string)indexes[0]]; - return true; - } - result = null; - return true; - } - - /// - /// Provides the implementation for operations that get member values. Classes derived from the class can override this method to specify dynamic behavior for operations such as getting a value for a property. - /// - /// Provides information about the object that called the dynamic operation. The binder.Name property provides the name of the member on which the dynamic operation is performed. For example, for the Console.WriteLine(sampleObject.SampleProperty) statement, where sampleObject is an instance of the class derived from the class, binder.Name returns "SampleProperty". The binder.IgnoreCase property specifies whether the member name is case-sensitive. - /// The result of the get operation. For example, if the method is called for a property, you can assign the property value to . - /// - /// Alwasy returns true. - /// - public override bool TryGetMember(GetMemberBinder binder, out object result) - { - object value; - if (_members.TryGetValue(binder.Name, out value)) - { - result = value; - return true; - } - result = null; - return true; - } - - /// - /// Provides the implementation for operations that set a value by index. Classes derived from the class can override this method to specify dynamic behavior for operations that access objects by a specified index. - /// - /// Provides information about the operation. - /// The indexes that are used in the operation. For example, for the sampleObject[3] = 10 operation in C# (sampleObject(3) = 10 in Visual Basic), where sampleObject is derived from the class, is equal to 3. - /// The value to set to the object that has the specified index. For example, for the sampleObject[3] = 10 operation in C# (sampleObject(3) = 10 in Visual Basic), where sampleObject is derived from the class, is equal to 10. - /// - /// true if the operation is successful; otherwise, false. If this method returns false, the run-time binder of the language determines the behavior. (In most cases, a language-specific run-time exception is thrown. - /// - public override bool TrySetIndex(SetIndexBinder binder, object[] indexes, object value) - { - if (indexes == null) throw new ArgumentNullException("indexes"); - if (indexes.Length == 1) - { - ((IDictionary)this)[(string)indexes[0]] = value; - return true; - } - return base.TrySetIndex(binder, indexes, value); - } - - /// - /// Provides the implementation for operations that set member values. Classes derived from the class can override this method to specify dynamic behavior for operations such as setting a value for a property. - /// - /// Provides information about the object that called the dynamic operation. The binder.Name property provides the name of the member to which the value is being assigned. For example, for the statement sampleObject.SampleProperty = "Test", where sampleObject is an instance of the class derived from the class, binder.Name returns "SampleProperty". The binder.IgnoreCase property specifies whether the member name is case-sensitive. - /// The value to set to the member. For example, for sampleObject.SampleProperty = "Test", where sampleObject is an instance of the class derived from the class, the is "Test". - /// - /// true if the operation is successful; otherwise, false. If this method returns false, the run-time binder of the language determines the behavior. (In most cases, a language-specific run-time exception is thrown.) - /// - public override bool TrySetMember(SetMemberBinder binder, object value) - { - // - if (binder == null) - throw new ArgumentNullException("binder"); - // - _members[binder.Name] = value; - return true; - } - - /// - /// Returns the enumeration of all dynamic member names. - /// - /// - /// A sequence that contains dynamic member names. - /// - public override IEnumerable GetDynamicMemberNames() - { - foreach (var key in Keys) - yield return key; - } -#endif - } -} - -namespace RestSharp.Tests -{ - /// - /// This class encodes and decodes JSON strings. - /// Spec. details, see http://www.json.org/ - /// - /// JSON uses Arrays and Objects. These correspond here to the datatypes JsonArray(IList<object>) and JsonObject(IDictionary<string,object>). - /// All numbers are parsed to doubles. - /// - [GeneratedCode("simple-json", "1.0.0")] -#if SIMPLE_JSON_INTERNAL - internal -#else - public -#endif - static class SimpleJson - { - private const int TOKEN_NONE = 0; - private const int TOKEN_CURLY_OPEN = 1; - private const int TOKEN_CURLY_CLOSE = 2; - private const int TOKEN_SQUARED_OPEN = 3; - private const int TOKEN_SQUARED_CLOSE = 4; - private const int TOKEN_COLON = 5; - private const int TOKEN_COMMA = 6; - private const int TOKEN_STRING = 7; - private const int TOKEN_NUMBER = 8; - private const int TOKEN_TRUE = 9; - private const int TOKEN_FALSE = 10; - private const int TOKEN_NULL = 11; - private const int BUILDER_CAPACITY = 2000; - - /// - /// Parses the string json into a value - /// - /// A JSON string. - /// An IList<object>, a IDictionary<string,object>, a double, a string, null, true, or false - public static object DeserializeObject(string json) - { - object obj; - if (TryDeserializeObject(json, out obj)) - return obj; - throw new SerializationException("Invalid JSON string"); - } - - /// - /// Try parsing the json string into a value. - /// - /// - /// A JSON string. - /// - /// - /// The object. - /// - /// - /// Returns true if successfull otherwise false. - /// - [SuppressMessage("Microsoft.Design", "CA1007:UseGenericsWhereAppropriate", Justification="Need to support .NET 2")] - public static bool TryDeserializeObject(string json, out object obj) - { - bool success = true; - if (json != null) - { - char[] charArray = json.ToCharArray(); - int index = 0; - obj = ParseValue(charArray, ref index, ref success); - } - else - obj = null; - - return success; - } - - public static object DeserializeObject(string json, Type type, IJsonSerializerStrategy jsonSerializerStrategy) - { - object jsonObject = DeserializeObject(json); - return type == null || jsonObject != null && ReflectionUtils.IsAssignableFrom(jsonObject.GetType(), type) - ? jsonObject - : (jsonSerializerStrategy ?? CurrentJsonSerializerStrategy).DeserializeObject(jsonObject, type); - } - - public static object DeserializeObject(string json, Type type) - { - return DeserializeObject(json, type, null); - } - - public static T DeserializeObject(string json, IJsonSerializerStrategy jsonSerializerStrategy) - { - return (T)DeserializeObject(json, typeof(T), jsonSerializerStrategy); - } - - public static T DeserializeObject(string json) - { - return (T)DeserializeObject(json, typeof(T), null); - } - - /// - /// Converts a IDictionary<string,object> / IList<object> object into a JSON string - /// - /// A IDictionary<string,object> / IList<object> - /// Serializer strategy to use - /// A JSON encoded string, or null if object 'json' is not serializable - public static string SerializeObject(object json, IJsonSerializerStrategy jsonSerializerStrategy) - { - StringBuilder builder = new StringBuilder(BUILDER_CAPACITY); - bool success = SerializeValue(jsonSerializerStrategy, json, builder); - return (success ? builder.ToString() : null); - } - - public static string SerializeObject(object json) - { - return SerializeObject(json, CurrentJsonSerializerStrategy); - } - - public static string EscapeToJavascriptString(string jsonString) - { - if (string.IsNullOrEmpty(jsonString)) - return jsonString; - - StringBuilder sb = new StringBuilder(); - char c; - - for (int i = 0; i < jsonString.Length; ) - { - c = jsonString[i++]; - - if (c == '\\') - { - int remainingLength = jsonString.Length - i; - if (remainingLength >= 2) - { - char lookahead = jsonString[i]; - if (lookahead == '\\') - { - sb.Append('\\'); - ++i; - } - else if (lookahead == '"') - { - sb.Append("\""); - ++i; - } - else if (lookahead == 't') - { - sb.Append('\t'); - ++i; - } - else if (lookahead == 'b') - { - sb.Append('\b'); - ++i; - } - else if (lookahead == 'n') - { - sb.Append('\n'); - ++i; - } - else if (lookahead == 'r') - { - sb.Append('\r'); - ++i; - } - } - } - else - { - sb.Append(c); - } - } - return sb.ToString(); - } - - static IDictionary ParseObject(char[] json, ref int index, ref bool success) - { - IDictionary table = new JsonObject(); - int token; - - // { - NextToken(json, ref index); - - bool done = false; - while (!done) - { - token = LookAhead(json, index); - if (token == TOKEN_NONE) - { - success = false; - return null; - } - else if (token == TOKEN_COMMA) - NextToken(json, ref index); - else if (token == TOKEN_CURLY_CLOSE) - { - NextToken(json, ref index); - return table; - } - else - { - // name - string name = ParseString(json, ref index, ref success); - if (!success) - { - success = false; - return null; - } - // : - token = NextToken(json, ref index); - if (token != TOKEN_COLON) - { - success = false; - return null; - } - // value - object value = ParseValue(json, ref index, ref success); - if (!success) - { - success = false; - return null; - } - table[name] = value; - } - } - return table; - } - - static JsonArray ParseArray(char[] json, ref int index, ref bool success) - { - JsonArray array = new JsonArray(); - - // [ - NextToken(json, ref index); - - bool done = false; - while (!done) - { - int token = LookAhead(json, index); - if (token == TOKEN_NONE) - { - success = false; - return null; - } - else if (token == TOKEN_COMMA) - NextToken(json, ref index); - else if (token == TOKEN_SQUARED_CLOSE) - { - NextToken(json, ref index); - break; - } - else - { - object value = ParseValue(json, ref index, ref success); - if (!success) - return null; - array.Add(value); - } - } - return array; - } - - static object ParseValue(char[] json, ref int index, ref bool success) - { - switch (LookAhead(json, index)) - { - case TOKEN_STRING: - return ParseString(json, ref index, ref success); - case TOKEN_NUMBER: - return ParseNumber(json, ref index, ref success); - case TOKEN_CURLY_OPEN: - return ParseObject(json, ref index, ref success); - case TOKEN_SQUARED_OPEN: - return ParseArray(json, ref index, ref success); - case TOKEN_TRUE: - NextToken(json, ref index); - return true; - case TOKEN_FALSE: - NextToken(json, ref index); - return false; - case TOKEN_NULL: - NextToken(json, ref index); - return null; - case TOKEN_NONE: - break; - } - success = false; - return null; - } - - static string ParseString(char[] json, ref int index, ref bool success) - { - StringBuilder s = new StringBuilder(BUILDER_CAPACITY); - char c; - - EatWhitespace(json, ref index); - - // " - c = json[index++]; - bool complete = false; - while (!complete) - { - if (index == json.Length) - break; - - c = json[index++]; - if (c == '"') - { - complete = true; - break; - } - else if (c == '\\') - { - if (index == json.Length) - break; - c = json[index++]; - if (c == '"') - s.Append('"'); - else if (c == '\\') - s.Append('\\'); - else if (c == '/') - s.Append('/'); - else if (c == 'b') - s.Append('\b'); - else if (c == 'f') - s.Append('\f'); - else if (c == 'n') - s.Append('\n'); - else if (c == 'r') - s.Append('\r'); - else if (c == 't') - s.Append('\t'); - else if (c == 'u') - { - int remainingLength = json.Length - index; - if (remainingLength >= 4) - { - // parse the 32 bit hex into an integer codepoint - uint codePoint; - if (!(success = UInt32.TryParse(new string(json, index, 4), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out codePoint))) - return ""; - - // convert the integer codepoint to a unicode char and add to string - if (0xD800 <= codePoint && codePoint <= 0xDBFF) // if high surrogate - { - index += 4; // skip 4 chars - remainingLength = json.Length - index; - if (remainingLength >= 6) - { - uint lowCodePoint; - if (new string(json, index, 2) == "\\u" && UInt32.TryParse(new string(json, index + 2, 4), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out lowCodePoint)) - { - if (0xDC00 <= lowCodePoint && lowCodePoint <= 0xDFFF) // if low surrogate - { - s.Append((char)codePoint); - s.Append((char)lowCodePoint); - index += 6; // skip 6 chars - continue; - } - } - } - success = false; // invalid surrogate pair - return ""; - } - s.Append(ConvertFromUtf32((int)codePoint)); - // skip 4 chars - index += 4; - } - else - break; - } - } - else - s.Append(c); - } - if (!complete) - { - success = false; - return null; - } - return s.ToString(); - } - - private static string ConvertFromUtf32(int utf32) - { - // http://www.java2s.com/Open-Source/CSharp/2.6.4-mono-.net-core/System/System/Char.cs.htm - if (utf32 < 0 || utf32 > 0x10FFFF) - throw new ArgumentOutOfRangeException("utf32", "The argument must be from 0 to 0x10FFFF."); - if (0xD800 <= utf32 && utf32 <= 0xDFFF) - throw new ArgumentOutOfRangeException("utf32", "The argument must not be in surrogate pair range."); - if (utf32 < 0x10000) - return new string((char)utf32, 1); - utf32 -= 0x10000; - return new string(new char[] { (char)((utf32 >> 10) + 0xD800), (char)(utf32 % 0x0400 + 0xDC00) }); - } - - static object ParseNumber(char[] json, ref int index, ref bool success) - { - EatWhitespace(json, ref index); - int lastIndex = GetLastIndexOfNumber(json, index); - int charLength = (lastIndex - index) + 1; - object returnNumber; - string str = new string(json, index, charLength); - if (str.IndexOf(".", StringComparison.OrdinalIgnoreCase) != -1 || str.IndexOf("e", StringComparison.OrdinalIgnoreCase) != -1) - { - double number; - success = double.TryParse(new string(json, index, charLength), NumberStyles.Any, CultureInfo.InvariantCulture, out number); - returnNumber = number; - } - else - { - long number; - success = long.TryParse(new string(json, index, charLength), NumberStyles.Any, CultureInfo.InvariantCulture, out number); - returnNumber = number; - } - index = lastIndex + 1; - return returnNumber; - } - - static int GetLastIndexOfNumber(char[] json, int index) - { - int lastIndex; - for (lastIndex = index; lastIndex < json.Length; lastIndex++) - if ("0123456789+-.eE".IndexOf(json[lastIndex]) == -1) break; - return lastIndex - 1; - } - - static void EatWhitespace(char[] json, ref int index) - { - for (; index < json.Length; index++) - if (" \t\n\r\b\f".IndexOf(json[index]) == -1) break; - } - - static int LookAhead(char[] json, int index) - { - int saveIndex = index; - return NextToken(json, ref saveIndex); - } - - [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")] - static int NextToken(char[] json, ref int index) - { - EatWhitespace(json, ref index); - if (index == json.Length) - return TOKEN_NONE; - char c = json[index]; - index++; - switch (c) - { - case '{': - return TOKEN_CURLY_OPEN; - case '}': - return TOKEN_CURLY_CLOSE; - case '[': - return TOKEN_SQUARED_OPEN; - case ']': - return TOKEN_SQUARED_CLOSE; - case ',': - return TOKEN_COMMA; - case '"': - return TOKEN_STRING; - case '0': - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': - case '8': - case '9': - case '-': - return TOKEN_NUMBER; - case ':': - return TOKEN_COLON; - } - index--; - int remainingLength = json.Length - index; - // false - if (remainingLength >= 5) - { - if (json[index] == 'f' && json[index + 1] == 'a' && json[index + 2] == 'l' && json[index + 3] == 's' && json[index + 4] == 'e') - { - index += 5; - return TOKEN_FALSE; - } - } - // true - if (remainingLength >= 4) - { - if (json[index] == 't' && json[index + 1] == 'r' && json[index + 2] == 'u' && json[index + 3] == 'e') - { - index += 4; - return TOKEN_TRUE; - } - } - // null - if (remainingLength >= 4) - { - if (json[index] == 'n' && json[index + 1] == 'u' && json[index + 2] == 'l' && json[index + 3] == 'l') - { - index += 4; - return TOKEN_NULL; - } - } - return TOKEN_NONE; - } - - static bool SerializeValue(IJsonSerializerStrategy jsonSerializerStrategy, object value, StringBuilder builder) - { - bool success = true; - string stringValue = value as string; - if (stringValue != null) - success = SerializeString(stringValue, builder); - else - { - IDictionary dict = value as IDictionary; - if (dict != null) - { - success = SerializeObject(jsonSerializerStrategy, dict.Keys, dict.Values, builder); - } - else - { - IDictionary stringDictionary = value as IDictionary; - if (stringDictionary != null) - { - success = SerializeObject(jsonSerializerStrategy, stringDictionary.Keys, stringDictionary.Values, builder); - } - else - { - IEnumerable enumerableValue = value as IEnumerable; - if (enumerableValue != null) - success = SerializeArray(jsonSerializerStrategy, enumerableValue, builder); - else if (IsNumeric(value)) - success = SerializeNumber(value, builder); - else if (value is bool) - builder.Append((bool)value ? "true" : "false"); - else if (value == null) - builder.Append("null"); - else - { - object serializedObject; - success = jsonSerializerStrategy.TrySerializeNonPrimitiveObject(value, out serializedObject); - if (success) - SerializeValue(jsonSerializerStrategy, serializedObject, builder); - } - } - } - } - return success; - } - - static bool SerializeObject(IJsonSerializerStrategy jsonSerializerStrategy, IEnumerable keys, IEnumerable values, StringBuilder builder) - { - builder.Append("{"); - IEnumerator ke = keys.GetEnumerator(); - IEnumerator ve = values.GetEnumerator(); - bool first = true; - while (ke.MoveNext() && ve.MoveNext()) - { - object key = ke.Current; - object value = ve.Current; - if (!first) - builder.Append(","); - string stringKey = key as string; - if (stringKey != null) - SerializeString(stringKey, builder); - else - if (!SerializeValue(jsonSerializerStrategy, value, builder)) return false; - builder.Append(":"); - if (!SerializeValue(jsonSerializerStrategy, value, builder)) - return false; - first = false; - } - builder.Append("}"); - return true; - } - - static bool SerializeArray(IJsonSerializerStrategy jsonSerializerStrategy, IEnumerable anArray, StringBuilder builder) - { - builder.Append("["); - bool first = true; - foreach (object value in anArray) - { - if (!first) - builder.Append(","); - if (!SerializeValue(jsonSerializerStrategy, value, builder)) - return false; - first = false; - } - builder.Append("]"); - return true; - } - - static bool SerializeString(string aString, StringBuilder builder) - { - builder.Append("\""); - char[] charArray = aString.ToCharArray(); - for (int i = 0; i < charArray.Length; i++) - { - char c = charArray[i]; - if (c == '"') - builder.Append("\\\""); - else if (c == '\\') - builder.Append("\\\\"); - else if (c == '\b') - builder.Append("\\b"); - else if (c == '\f') - builder.Append("\\f"); - else if (c == '\n') - builder.Append("\\n"); - else if (c == '\r') - builder.Append("\\r"); - else if (c == '\t') - builder.Append("\\t"); - else - builder.Append(c); - } - builder.Append("\""); - return true; - } - - static bool SerializeNumber(object number, StringBuilder builder) - { - if (number is long) - builder.Append(((long)number).ToString(CultureInfo.InvariantCulture)); - else if (number is ulong) - builder.Append(((ulong)number).ToString(CultureInfo.InvariantCulture)); - else if (number is int) - builder.Append(((int)number).ToString(CultureInfo.InvariantCulture)); - else if (number is uint) - builder.Append(((uint)number).ToString(CultureInfo.InvariantCulture)); - else if (number is decimal) - builder.Append(((decimal)number).ToString(CultureInfo.InvariantCulture)); - else if (number is float) - builder.Append(((float)number).ToString(CultureInfo.InvariantCulture)); - else - builder.Append(Convert.ToDouble(number, CultureInfo.InvariantCulture).ToString("r", CultureInfo.InvariantCulture)); - return true; - } - - /// - /// Determines if a given object is numeric in any way - /// (can be integer, double, null, etc). - /// - static bool IsNumeric(object value) - { - if (value is sbyte) return true; - if (value is byte) return true; - if (value is short) return true; - if (value is ushort) return true; - if (value is int) return true; - if (value is uint) return true; - if (value is long) return true; - if (value is ulong) return true; - if (value is float) return true; - if (value is double) return true; - if (value is decimal) return true; - return false; - } - - private static IJsonSerializerStrategy _currentJsonSerializerStrategy; - public static IJsonSerializerStrategy CurrentJsonSerializerStrategy - { - get - { - return _currentJsonSerializerStrategy ?? - (_currentJsonSerializerStrategy = -#if SIMPLE_JSON_DATACONTRACT - DataContractJsonSerializerStrategy -#else - PocoJsonSerializerStrategy -#endif -); - } - set - { - _currentJsonSerializerStrategy = value; - } - } - - private static PocoJsonSerializerStrategy _pocoJsonSerializerStrategy; - [EditorBrowsable(EditorBrowsableState.Advanced)] - public static PocoJsonSerializerStrategy PocoJsonSerializerStrategy - { - get - { - return _pocoJsonSerializerStrategy ?? (_pocoJsonSerializerStrategy = new PocoJsonSerializerStrategy()); - } - } - -#if SIMPLE_JSON_DATACONTRACT - - private static DataContractJsonSerializerStrategy _dataContractJsonSerializerStrategy; - [System.ComponentModel.EditorBrowsable(EditorBrowsableState.Advanced)] - public static DataContractJsonSerializerStrategy DataContractJsonSerializerStrategy - { - get - { - return _dataContractJsonSerializerStrategy ?? (_dataContractJsonSerializerStrategy = new DataContractJsonSerializerStrategy()); - } - } - -#endif - } - - [GeneratedCode("simple-json", "1.0.0")] -#if SIMPLE_JSON_INTERNAL - internal -#else - public -#endif - interface IJsonSerializerStrategy - { - [SuppressMessage("Microsoft.Design", "CA1007:UseGenericsWhereAppropriate", Justification="Need to support .NET 2")] - bool TrySerializeNonPrimitiveObject(object input, out object output); - object DeserializeObject(object value, Type type); - } - - [GeneratedCode("simple-json", "1.0.0")] -#if SIMPLE_JSON_INTERNAL - internal -#else - public -#endif - class PocoJsonSerializerStrategy : IJsonSerializerStrategy - { - internal IDictionary ConstructorCache; - internal IDictionary> GetCache; - internal IDictionary>> SetCache; - - internal static readonly Type[] EmptyTypes = new Type[0]; - internal static readonly Type[] ArrayConstructorParameterTypes = new Type[] { typeof(int) }; - - private static readonly string[] Iso8601Format = new string[] - { - @"yyyy-MM-dd\THH:mm:ss.FFFFFFF\Z", - @"yyyy-MM-dd\THH:mm:ss\Z", - @"yyyy-MM-dd\THH:mm:ssK" - }; - - public PocoJsonSerializerStrategy() - { - ConstructorCache = new ReflectionUtils.ThreadSafeDictionary(ContructorDelegateFactory); - GetCache = new ReflectionUtils.ThreadSafeDictionary>(GetterValueFactory); - SetCache = new ReflectionUtils.ThreadSafeDictionary>>(SetterValueFactory); - } - - protected virtual string MapClrMemberNameToJsonFieldName(string clrPropertyName) - { - return clrPropertyName; - } - - internal virtual ReflectionUtils.ConstructorDelegate ContructorDelegateFactory(Type key) - { - return ReflectionUtils.GetContructor(key, key.IsArray ? ArrayConstructorParameterTypes : EmptyTypes); - } - - internal virtual IDictionary GetterValueFactory(Type type) - { - IDictionary result = new Dictionary(); - foreach (PropertyInfo propertyInfo in ReflectionUtils.GetProperties(type)) - { - if (propertyInfo.CanRead) - { - MethodInfo getMethod = ReflectionUtils.GetGetterMethodInfo(propertyInfo); - if (getMethod.IsStatic || !getMethod.IsPublic) - continue; - result[MapClrMemberNameToJsonFieldName(propertyInfo.Name)] = ReflectionUtils.GetGetMethod(propertyInfo); - } - } - foreach (FieldInfo fieldInfo in ReflectionUtils.GetFields(type)) - { - if (fieldInfo.IsStatic || !fieldInfo.IsPublic) - continue; - result[MapClrMemberNameToJsonFieldName(fieldInfo.Name)] = ReflectionUtils.GetGetMethod(fieldInfo); - } - return result; - } - - internal virtual IDictionary> SetterValueFactory(Type type) - { - IDictionary> result = new Dictionary>(); - foreach (PropertyInfo propertyInfo in ReflectionUtils.GetProperties(type)) - { - if (propertyInfo.CanWrite) - { - MethodInfo setMethod = ReflectionUtils.GetSetterMethodInfo(propertyInfo); - if (setMethod.IsStatic || !setMethod.IsPublic) - continue; - result[MapClrMemberNameToJsonFieldName(propertyInfo.Name)] = new KeyValuePair(propertyInfo.PropertyType, ReflectionUtils.GetSetMethod(propertyInfo)); - } - } - foreach (FieldInfo fieldInfo in ReflectionUtils.GetFields(type)) - { - if (fieldInfo.IsInitOnly || fieldInfo.IsStatic || !fieldInfo.IsPublic) - continue; - result[MapClrMemberNameToJsonFieldName(fieldInfo.Name)] = new KeyValuePair(fieldInfo.FieldType, ReflectionUtils.GetSetMethod(fieldInfo)); - } - return result; - } - - public virtual bool TrySerializeNonPrimitiveObject(object input, out object output) - { - return TrySerializeKnownTypes(input, out output) || TrySerializeUnknownTypes(input, out output); - } - - [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")] - public virtual object DeserializeObject(object value, Type type) - { - if (type == null) throw new ArgumentNullException("type"); - string str = value as string; - - if (type == typeof (Guid) && string.IsNullOrEmpty(str)) - return default(Guid); - - if (value == null) - return null; - - object obj = null; - - if (str != null) - { - if (str.Length != 0) // We know it can't be null now. - { - if (type == typeof(DateTime) || (ReflectionUtils.IsNullableType(type) && Nullable.GetUnderlyingType(type) == typeof(DateTime))) - return DateTime.ParseExact(str, Iso8601Format, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal); - if (type == typeof(DateTimeOffset) || (ReflectionUtils.IsNullableType(type) && Nullable.GetUnderlyingType(type) == typeof(DateTimeOffset))) - return DateTimeOffset.ParseExact(str, Iso8601Format, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal); - if (type == typeof(Guid) || (ReflectionUtils.IsNullableType(type) && Nullable.GetUnderlyingType(type) == typeof(Guid))) - return new Guid(str); - return str; - } - else - { - if (type == typeof(Guid)) - obj = default(Guid); - else if (ReflectionUtils.IsNullableType(type) && Nullable.GetUnderlyingType(type) == typeof(Guid)) - obj = null; - else - obj = str; - } - // Empty string case - if (!ReflectionUtils.IsNullableType(type) && Nullable.GetUnderlyingType(type) == typeof(Guid)) - return str; - } - else if (value is bool) - return value; - - bool valueIsLong = value is long; - bool valueIsDouble = value is double; - if ((valueIsLong && type == typeof(long)) || (valueIsDouble && type == typeof(double))) - return value; - if ((valueIsDouble && type != typeof(double)) || (valueIsLong && type != typeof(long))) - { - obj = type == typeof(int) || type == typeof(long) || type == typeof(double) || type == typeof(float) || type == typeof(bool) || type == typeof(decimal) || type == typeof(byte) || type == typeof(short) - ? Convert.ChangeType(value, type, CultureInfo.InvariantCulture) - : value; - } - else - { - IDictionary objects = value as IDictionary; - if (objects != null) - { - IDictionary jsonObject = objects; - - if (ReflectionUtils.IsTypeDictionary(type)) - { - // if dictionary then - Type[] types = ReflectionUtils.GetGenericTypeArguments(type); - Type keyType = types[0]; - Type valueType = types[1]; - - Type genericType = typeof(Dictionary<,>).MakeGenericType(keyType, valueType); - - IDictionary dict = (IDictionary)ConstructorCache[genericType](); - - foreach (KeyValuePair kvp in jsonObject) - dict.Add(kvp.Key, DeserializeObject(kvp.Value, valueType)); - - obj = dict; - } - else - { - if (type == typeof(object)) - obj = value; - else - { - obj = ConstructorCache[type](); - foreach (KeyValuePair> setter in SetCache[type]) - { - object jsonValue; - if (jsonObject.TryGetValue(setter.Key, out jsonValue)) - { - jsonValue = DeserializeObject(jsonValue, setter.Value.Key); - setter.Value.Value(obj, jsonValue); - } - } - } - } - } - else - { - IList valueAsList = value as IList; - if (valueAsList != null) - { - IList jsonObject = valueAsList; - IList list = null; - - if (type.IsArray) - { - list = (IList)ConstructorCache[type](jsonObject.Count); - int i = 0; - foreach (object o in jsonObject) - list[i++] = DeserializeObject(o, type.GetElementType()); - } - else if (ReflectionUtils.IsTypeGenericeCollectionInterface(type) || ReflectionUtils.IsAssignableFrom(typeof(IList), type)) - { - Type innerType = ReflectionUtils.GetGenericTypeArguments(type)[0]; - Type genericType = typeof(List<>).MakeGenericType(innerType); - list = (IList)ConstructorCache[genericType](jsonObject.Count); - foreach (object o in jsonObject) - list.Add(DeserializeObject(o, innerType)); - } - obj = list; - } - } - return obj; - } - if (ReflectionUtils.IsNullableType(type)) - return ReflectionUtils.ToNullableType(obj, type); - return obj; - } - - protected virtual object SerializeEnum(Enum p) - { - return Convert.ToDouble(p, CultureInfo.InvariantCulture); - } - - [SuppressMessage("Microsoft.Design", "CA1007:UseGenericsWhereAppropriate", Justification="Need to support .NET 2")] - protected virtual bool TrySerializeKnownTypes(object input, out object output) - { - bool returnValue = true; - if (input is DateTime) - output = ((DateTime)input).ToUniversalTime().ToString(Iso8601Format[0], CultureInfo.InvariantCulture); - else if (input is DateTimeOffset) - output = ((DateTimeOffset)input).ToUniversalTime().ToString(Iso8601Format[0], CultureInfo.InvariantCulture); - else if (input is Guid) - output = ((Guid)input).ToString("D"); - else if (input is Uri) - output = input.ToString(); - else - { - Enum inputEnum = input as Enum; - if (inputEnum != null) - output = SerializeEnum(inputEnum); - else - { - returnValue = false; - output = null; - } - } - return returnValue; - } - [SuppressMessage("Microsoft.Design", "CA1007:UseGenericsWhereAppropriate", Justification="Need to support .NET 2")] - protected virtual bool TrySerializeUnknownTypes(object input, out object output) - { - if (input == null) throw new ArgumentNullException("input"); - output = null; - Type type = input.GetType(); - if (type.FullName == null) - return false; - IDictionary obj = new JsonObject(); - IDictionary getters = GetCache[type]; - foreach (KeyValuePair getter in getters) - { - if (getter.Value != null) - obj.Add(MapClrMemberNameToJsonFieldName(getter.Key), getter.Value(input)); - } - output = obj; - return true; - } - } - -#if SIMPLE_JSON_DATACONTRACT - [GeneratedCode("simple-json", "1.0.0")] -#if SIMPLE_JSON_INTERNAL - internal -#else - public -#endif - class DataContractJsonSerializerStrategy : PocoJsonSerializerStrategy - { - public DataContractJsonSerializerStrategy() - { - GetCache = new ReflectionUtils.ThreadSafeDictionary>(GetterValueFactory); - SetCache = new ReflectionUtils.ThreadSafeDictionary>>(SetterValueFactory); - } - - internal override IDictionary GetterValueFactory(Type type) - { - bool hasDataContract = ReflectionUtils.GetAttribute(type, typeof(DataContractAttribute)) != null; - if (!hasDataContract) - return base.GetterValueFactory(type); - string jsonKey; - IDictionary result = new Dictionary(); - foreach (PropertyInfo propertyInfo in ReflectionUtils.GetProperties(type)) - { - if (propertyInfo.CanRead) - { - MethodInfo getMethod = ReflectionUtils.GetGetterMethodInfo(propertyInfo); - if (!getMethod.IsStatic && CanAdd(propertyInfo, out jsonKey)) - result[jsonKey] = ReflectionUtils.GetGetMethod(propertyInfo); - } - } - foreach (FieldInfo fieldInfo in ReflectionUtils.GetFields(type)) - { - if (!fieldInfo.IsStatic && CanAdd(fieldInfo, out jsonKey)) - result[jsonKey] = ReflectionUtils.GetGetMethod(fieldInfo); - } - return result; - } - - internal override IDictionary> SetterValueFactory(Type type) - { - bool hasDataContract = ReflectionUtils.GetAttribute(type, typeof(DataContractAttribute)) != null; - if (!hasDataContract) - return base.SetterValueFactory(type); - string jsonKey; - IDictionary> result = new Dictionary>(); - foreach (PropertyInfo propertyInfo in ReflectionUtils.GetProperties(type)) - { - if (propertyInfo.CanWrite) - { - MethodInfo setMethod = ReflectionUtils.GetSetterMethodInfo(propertyInfo); - if (!setMethod.IsStatic && CanAdd(propertyInfo, out jsonKey)) - result[jsonKey] = new KeyValuePair(propertyInfo.PropertyType, ReflectionUtils.GetSetMethod(propertyInfo)); - } - } - foreach (FieldInfo fieldInfo in ReflectionUtils.GetFields(type)) - { - if (!fieldInfo.IsInitOnly && !fieldInfo.IsStatic && CanAdd(fieldInfo, out jsonKey)) - result[jsonKey] = new KeyValuePair(fieldInfo.FieldType, ReflectionUtils.GetSetMethod(fieldInfo)); - } - // todo implement sorting for DATACONTRACT. - return result; - } - - private static bool CanAdd(MemberInfo info, out string jsonKey) - { - jsonKey = null; - if (ReflectionUtils.GetAttribute(info, typeof(IgnoreDataMemberAttribute)) != null) - return false; - DataMemberAttribute dataMemberAttribute = (DataMemberAttribute)ReflectionUtils.GetAttribute(info, typeof(DataMemberAttribute)); - if (dataMemberAttribute == null) - return false; - jsonKey = string.IsNullOrEmpty(dataMemberAttribute.Name) ? info.Name : dataMemberAttribute.Name; - return true; - } - } - -#endif - - namespace Reflection - { - // This class is meant to be copied into other libraries. So we want to exclude it from Code Analysis rules - // that might be in place in the target project. - [GeneratedCode("reflection-utils", "1.0.0")] -#if SIMPLE_JSON_REFLECTION_UTILS_PUBLIC - public -#else - internal -#endif - class ReflectionUtils - { - private static readonly object[] EmptyObjects = new object[] { }; - - public delegate object GetDelegate(object source); - public delegate void SetDelegate(object source, object value); - public delegate object ConstructorDelegate(params object[] args); - - public delegate TValue ThreadSafeDictionaryValueFactory(TKey key); - - public static Attribute GetAttribute(MemberInfo info, Type type) - { -#if SIMPLE_JSON_TYPEINFO - if (info == null || type == null || !info.IsDefined(type)) - return null; - return info.GetCustomAttribute(type); -#else - if (info == null || type == null || !Attribute.IsDefined(info, type)) - return null; - return Attribute.GetCustomAttribute(info, type); -#endif - } - - public static Attribute GetAttribute(Type objectType, Type attributeType) - { - -#if SIMPLE_JSON_TYPEINFO - if (objectType == null || attributeType == null || !objectType.GetTypeInfo().IsDefined(attributeType)) - return null; - return objectType.GetTypeInfo().GetCustomAttribute(attributeType); -#else - if (objectType == null || attributeType == null || !Attribute.IsDefined(objectType, attributeType)) - return null; - return Attribute.GetCustomAttribute(objectType, attributeType); -#endif - } - - public static Type[] GetGenericTypeArguments(Type type) - { -#if SIMPLE_JSON_TYPEINFO - return type.GetTypeInfo().GenericTypeArguments; -#else - return type.GetGenericArguments(); -#endif - } - - public static bool IsTypeGenericeCollectionInterface(Type type) - { -#if SIMPLE_JSON_TYPEINFO - if (!type.GetTypeInfo().IsGenericType) -#else - if (!type.IsGenericType) -#endif - return false; - - Type genericDefinition = type.GetGenericTypeDefinition(); - - return (genericDefinition == typeof(IList<>) || genericDefinition == typeof(ICollection<>) || genericDefinition == typeof(IEnumerable<>)); - } - - public static bool IsAssignableFrom(Type type1, Type type2) - { -#if SIMPLE_JSON_TYPEINFO - return type1.GetTypeInfo().IsAssignableFrom(type2.GetTypeInfo()); -#else - return type1.IsAssignableFrom(type2); -#endif - } - - public static bool IsTypeDictionary(Type type) - { -#if SIMPLE_JSON_TYPEINFO - if (typeof(IDictionary<,>).GetTypeInfo().IsAssignableFrom(type.GetTypeInfo())) - return true; - - if (!type.GetTypeInfo().IsGenericType) - return false; -#else - if (typeof(System.Collections.IDictionary).IsAssignableFrom(type)) - return true; - - if (!type.IsGenericType) - return false; -#endif - Type genericDefinition = type.GetGenericTypeDefinition(); - return genericDefinition == typeof(IDictionary<,>); - } - - public static bool IsNullableType(Type type) - { - return -#if SIMPLE_JSON_TYPEINFO - type.GetTypeInfo().IsGenericType -#else - type.IsGenericType -#endif - && type.GetGenericTypeDefinition() == typeof(Nullable<>); - } - - public static object ToNullableType(object obj, Type nullableType) - { - return obj == null ? null : Convert.ChangeType(obj, Nullable.GetUnderlyingType(nullableType), CultureInfo.InvariantCulture); - } - - public static bool IsValueType(Type type) - { -#if SIMPLE_JSON_TYPEINFO - return type.GetTypeInfo().IsValueType; -#else - return type.IsValueType; -#endif - } - - public static IEnumerable GetConstructors(Type type) - { -#if SIMPLE_JSON_TYPEINFO - return type.GetTypeInfo().DeclaredConstructors; -#else - return type.GetConstructors(); -#endif - } - - public static ConstructorInfo GetConstructorInfo(Type type, params Type[] argsType) - { - IEnumerable constructorInfos = GetConstructors(type); - int i; - bool matches; - foreach (ConstructorInfo constructorInfo in constructorInfos) - { - ParameterInfo[] parameters = constructorInfo.GetParameters(); - if (argsType.Length != parameters.Length) - continue; - - i = 0; - matches = true; - foreach (ParameterInfo parameterInfo in constructorInfo.GetParameters()) - { - if (parameterInfo.ParameterType != argsType[i]) - { - matches = false; - break; - } - } - - if (matches) - return constructorInfo; - } - - return null; - } - - public static IEnumerable GetProperties(Type type) - { -#if SIMPLE_JSON_TYPEINFO - return type.GetTypeInfo().DeclaredProperties; -#else - return type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static); -#endif - } - - public static IEnumerable GetFields(Type type) - { -#if SIMPLE_JSON_TYPEINFO - return type.GetTypeInfo().DeclaredFields; -#else - return type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static); -#endif - } - - public static MethodInfo GetGetterMethodInfo(PropertyInfo propertyInfo) - { -#if SIMPLE_JSON_TYPEINFO - return propertyInfo.GetMethod; -#else - return propertyInfo.GetGetMethod(true); -#endif - } - - public static MethodInfo GetSetterMethodInfo(PropertyInfo propertyInfo) - { -#if SIMPLE_JSON_TYPEINFO - return propertyInfo.SetMethod; -#else - return propertyInfo.GetSetMethod(true); -#endif - } - - public static ConstructorDelegate GetContructor(ConstructorInfo constructorInfo) - { -#if SIMPLE_JSON_NO_LINQ_EXPRESSION - return GetConstructorByReflection(constructorInfo); -#else - return GetConstructorByExpression(constructorInfo); -#endif - } - - public static ConstructorDelegate GetContructor(Type type, params Type[] argsType) - { -#if SIMPLE_JSON_NO_LINQ_EXPRESSION - return GetConstructorByReflection(type, argsType); -#else - return GetConstructorByExpression(type, argsType); -#endif - } - - public static ConstructorDelegate GetConstructorByReflection(ConstructorInfo constructorInfo) - { - return delegate(object[] args) { return constructorInfo.Invoke(args); }; - } - - public static ConstructorDelegate GetConstructorByReflection(Type type, params Type[] argsType) - { - ConstructorInfo constructorInfo = GetConstructorInfo(type, argsType); - return constructorInfo == null ? null : GetConstructorByReflection(constructorInfo); - } - -#if !SIMPLE_JSON_NO_LINQ_EXPRESSION - - public static ConstructorDelegate GetConstructorByExpression(ConstructorInfo constructorInfo) - { - ParameterInfo[] paramsInfo = constructorInfo.GetParameters(); - ParameterExpression param = Expression.Parameter(typeof(object[]), "args"); - Expression[] argsExp = new Expression[paramsInfo.Length]; - for (int i = 0; i < paramsInfo.Length; i++) - { - Expression index = Expression.Constant(i); - Type paramType = paramsInfo[i].ParameterType; - Expression paramAccessorExp = Expression.ArrayIndex(param, index); - Expression paramCastExp = Expression.Convert(paramAccessorExp, paramType); - argsExp[i] = paramCastExp; - } - NewExpression newExp = Expression.New(constructorInfo, argsExp); - Expression> lambda = Expression.Lambda>(newExp, param); - Func compiledLambda = lambda.Compile(); - return delegate(object[] args) { return compiledLambda(args); }; - } - - public static ConstructorDelegate GetConstructorByExpression(Type type, params Type[] argsType) - { - ConstructorInfo constructorInfo = GetConstructorInfo(type, argsType); - return constructorInfo == null ? null : GetConstructorByExpression(constructorInfo); - } - -#endif - - public static GetDelegate GetGetMethod(PropertyInfo propertyInfo) - { -#if SIMPLE_JSON_NO_LINQ_EXPRESSION - return GetGetMethodByReflection(propertyInfo); -#else - return GetGetMethodByExpression(propertyInfo); -#endif - } - - public static GetDelegate GetGetMethod(FieldInfo fieldInfo) - { -#if SIMPLE_JSON_NO_LINQ_EXPRESSION - return GetGetMethodByReflection(fieldInfo); -#else - return GetGetMethodByExpression(fieldInfo); -#endif - } - - public static GetDelegate GetGetMethodByReflection(PropertyInfo propertyInfo) - { - MethodInfo methodInfo = GetGetterMethodInfo(propertyInfo); - return delegate(object source) { return methodInfo.Invoke(source, EmptyObjects); }; - } - - public static GetDelegate GetGetMethodByReflection(FieldInfo fieldInfo) - { - return delegate(object source) { return fieldInfo.GetValue(source); }; - } - -#if !SIMPLE_JSON_NO_LINQ_EXPRESSION - - public static GetDelegate GetGetMethodByExpression(PropertyInfo propertyInfo) - { - MethodInfo getMethodInfo = GetGetterMethodInfo(propertyInfo); - ParameterExpression instance = Expression.Parameter(typeof(object), "instance"); - UnaryExpression instanceCast = (!IsValueType(propertyInfo.DeclaringType)) ? Expression.TypeAs(instance, propertyInfo.DeclaringType) : Expression.Convert(instance, propertyInfo.DeclaringType); - Func compiled = Expression.Lambda>(Expression.TypeAs(Expression.Call(instanceCast, getMethodInfo), typeof(object)), instance).Compile(); - return delegate(object source) { return compiled(source); }; - } - - public static GetDelegate GetGetMethodByExpression(FieldInfo fieldInfo) - { - ParameterExpression instance = Expression.Parameter(typeof(object), "instance"); - MemberExpression member = Expression.Field(Expression.Convert(instance, fieldInfo.DeclaringType), fieldInfo); - GetDelegate compiled = Expression.Lambda(Expression.Convert(member, typeof(object)), instance).Compile(); - return delegate(object source) { return compiled(source); }; - } - -#endif - - public static SetDelegate GetSetMethod(PropertyInfo propertyInfo) - { -#if SIMPLE_JSON_NO_LINQ_EXPRESSION - return GetSetMethodByReflection(propertyInfo); -#else - return GetSetMethodByExpression(propertyInfo); -#endif - } - - public static SetDelegate GetSetMethod(FieldInfo fieldInfo) - { -#if SIMPLE_JSON_NO_LINQ_EXPRESSION - return GetSetMethodByReflection(fieldInfo); -#else - return GetSetMethodByExpression(fieldInfo); -#endif - } - - public static SetDelegate GetSetMethodByReflection(PropertyInfo propertyInfo) - { - MethodInfo methodInfo = GetSetterMethodInfo(propertyInfo); - return delegate(object source, object value) { methodInfo.Invoke(source, new object[] { value }); }; - } - - public static SetDelegate GetSetMethodByReflection(FieldInfo fieldInfo) - { - return delegate(object source, object value) { fieldInfo.SetValue(source, value); }; - } - -#if !SIMPLE_JSON_NO_LINQ_EXPRESSION - - public static SetDelegate GetSetMethodByExpression(PropertyInfo propertyInfo) - { - MethodInfo setMethodInfo = GetSetterMethodInfo(propertyInfo); - ParameterExpression instance = Expression.Parameter(typeof(object), "instance"); - ParameterExpression value = Expression.Parameter(typeof(object), "value"); - UnaryExpression instanceCast = (!IsValueType(propertyInfo.DeclaringType)) ? Expression.TypeAs(instance, propertyInfo.DeclaringType) : Expression.Convert(instance, propertyInfo.DeclaringType); - UnaryExpression valueCast = (!IsValueType(propertyInfo.PropertyType)) ? Expression.TypeAs(value, propertyInfo.PropertyType) : Expression.Convert(value, propertyInfo.PropertyType); - Action compiled = Expression.Lambda>(Expression.Call(instanceCast, setMethodInfo, valueCast), new ParameterExpression[] { instance, value }).Compile(); - return delegate(object source, object val) { compiled(source, val); }; - } - - public static SetDelegate GetSetMethodByExpression(FieldInfo fieldInfo) - { - ParameterExpression instance = Expression.Parameter(typeof(object), "instance"); - ParameterExpression value = Expression.Parameter(typeof(object), "value"); - Action compiled = Expression.Lambda>( - Assign(Expression.Field(Expression.Convert(instance, fieldInfo.DeclaringType), fieldInfo), Expression.Convert(value, fieldInfo.FieldType)), instance, value).Compile(); - return delegate(object source, object val) { compiled(source, val); }; - } - - public static BinaryExpression Assign(Expression left, Expression right) - { -#if SIMPLE_JSON_TYPEINFO - return Expression.Assign(left, right); -#else - MethodInfo assign = typeof(Assigner<>).MakeGenericType(left.Type).GetMethod("Assign"); - BinaryExpression assignExpr = Expression.Add(left, right, assign); - return assignExpr; -#endif - } - - private static class Assigner - { - public static T Assign(ref T left, T right) - { - return (left = right); - } - } - -#endif - - public sealed class ThreadSafeDictionary : IDictionary - { - private readonly object _lock = new object(); - private readonly ThreadSafeDictionaryValueFactory _valueFactory; - private Dictionary _dictionary; - - public ThreadSafeDictionary(ThreadSafeDictionaryValueFactory valueFactory) - { - _valueFactory = valueFactory; - } - - private TValue Get(TKey key) - { - if (_dictionary == null) - return AddValue(key); - TValue value; - if (!_dictionary.TryGetValue(key, out value)) - return AddValue(key); - return value; - } - - private TValue AddValue(TKey key) - { - TValue value = _valueFactory(key); - lock (_lock) - { - if (_dictionary == null) - { - _dictionary = new Dictionary(); - _dictionary[key] = value; - } - else - { - TValue val; - if (_dictionary.TryGetValue(key, out val)) - return val; - Dictionary dict = new Dictionary(_dictionary); - dict[key] = value; - _dictionary = dict; - } - } - return value; - } - - public void Add(TKey key, TValue value) - { - throw new NotImplementedException(); - } - - public bool ContainsKey(TKey key) - { - return _dictionary.ContainsKey(key); - } - - public ICollection Keys - { - get { return _dictionary.Keys; } - } - - public bool Remove(TKey key) - { - throw new NotImplementedException(); - } - - public bool TryGetValue(TKey key, out TValue value) - { - value = this[key]; - return true; - } - - public ICollection Values - { - get { return _dictionary.Values; } - } - - public TValue this[TKey key] - { - get { return Get(key); } - set { throw new NotImplementedException(); } - } - - public void Add(KeyValuePair item) - { - throw new NotImplementedException(); - } - - public void Clear() - { - throw new NotImplementedException(); - } - - public bool Contains(KeyValuePair item) - { - throw new NotImplementedException(); - } - - public void CopyTo(KeyValuePair[] array, int arrayIndex) - { - throw new NotImplementedException(); - } - - public int Count - { - get { return _dictionary.Count; } - } - - public bool IsReadOnly - { - get { throw new NotImplementedException(); } - } - - public bool Remove(KeyValuePair item) - { - throw new NotImplementedException(); - } - - public IEnumerator> GetEnumerator() - { - return _dictionary.GetEnumerator(); - } - - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() - { - return _dictionary.GetEnumerator(); - } - } - - } - } -} -// ReSharper restore LoopCanBeConvertedToQuery -// ReSharper restore RedundantExplicitArrayCreation -// ReSharper restore SuggestUseVarKeywordEvident diff --git a/RestSharp.Tests/StringExtensionsTests.cs b/RestSharp.Tests/StringExtensionsTests.cs deleted file mode 100644 index 7409dfa04..000000000 --- a/RestSharp.Tests/StringExtensionsTests.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Text; -using RestSharp.Extensions; -using Xunit; - -namespace RestSharp.Tests -{ - public class StringExtensionsTests - { - [Fact] - public void UrlEncode_Throws_ArgumentNullException_For_Null_Input() - { - const string nullString = null; - Assert.Throws( - delegate - { - nullString.UrlEncode(); - }); - } - - [Fact] - public void UrlEncode_Returns_Correct_Length_When_Less_Than_Limit() - { - const int numLessThanLimit = 32766; - string stringWithLimitLength = new string('*', numLessThanLimit); - Assert.True(stringWithLimitLength.UrlEncode().Length == numLessThanLimit); - } - - [Fact] - public void UrlEncode_Returns_Correct_Length_When_More_Than_Limit() - { - const int numGreaterThanLimit = 65000; - string stringWithLimitLength = new string('*', numGreaterThanLimit); - Assert.True(stringWithLimitLength.UrlEncode().Length == numGreaterThanLimit); - } - } -} diff --git a/RestSharp.Tests/UrlBuilderTests.cs b/RestSharp.Tests/UrlBuilderTests.cs deleted file mode 100644 index 438e1646b..000000000 --- a/RestSharp.Tests/UrlBuilderTests.cs +++ /dev/null @@ -1,183 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Xunit; - -namespace RestSharp.Tests -{ - /// - /// Note: These tests do not handle QueryString building, which is handled in Http, not RestClient - /// - public class UrlBuilderTests - { - [Fact] - public void GET_with_leading_slash() - { - var request = new RestRequest("/resource"); - var client = new RestClient("http://example.com"); - - var expected = new Uri("http://example.com/resource"); - var output = client.BuildUri(request); - - Assert.Equal(expected, output); - } - - [Fact] - public void POST_with_leading_slash() - { - var request = new RestRequest("/resource", Method.POST); - var client = new RestClient("http://example.com"); - - var expected = new Uri("http://example.com/resource"); - var output = client.BuildUri(request); - - Assert.Equal(expected, output); - } - - [Fact] - public void GET_with_leading_slash_and_baseurl_trailing_slash() - { - var request = new RestRequest("/resource"); - request.AddParameter("foo", "bar"); - var client = new RestClient("http://example.com/"); - - var expected = new Uri("http://example.com/resource?foo=bar"); - var output = client.BuildUri(request); - - Assert.Equal(expected, output); - } - - [Fact] - public void GET_wth_trailing_slash_and_query_parameters() - { - var request = new RestRequest("/resource/"); - var client = new RestClient("http://example.com"); - request.AddParameter("foo", "bar"); - - var expected = new Uri("http://example.com/resource/?foo=bar"); - var output = client.BuildUri(request); - - var response = client.Execute(request); - - Assert.Equal(expected, output); - } - - [Fact] - public void POST_with_leading_slash_and_baseurl_trailing_slash() - { - var request = new RestRequest("/resource", Method.POST); - var client = new RestClient("http://example.com/"); - - var expected = new Uri("http://example.com/resource"); - var output = client.BuildUri(request); - - Assert.Equal(expected, output); - } - - [Fact] - public void GET_with_resource_containing_slashes() - { - var request = new RestRequest("resource/foo"); - var client = new RestClient("http://example.com"); - - var expected = new Uri("http://example.com/resource/foo"); - var output = client.BuildUri(request); - - Assert.Equal(expected, output); - } - - [Fact] - public void POST_with_resource_containing_slashes() - { - var request = new RestRequest("resource/foo", Method.POST); - var client = new RestClient("http://example.com"); - - var expected = new Uri("http://example.com/resource/foo"); - var output = client.BuildUri(request); - - Assert.Equal(expected, output); - } - - [Fact] - public void GET_with_resource_containing_tokens() - { - var request = new RestRequest("resource/{foo}"); - request.AddUrlSegment("foo", "bar"); - var client = new RestClient("http://example.com"); - - var expected = new Uri("http://example.com/resource/bar"); - var output = client.BuildUri(request); - - Assert.Equal(expected, output); - } - - [Fact] - public void POST_with_resource_containing_tokens() - { - var request = new RestRequest("resource/{foo}", Method.POST); - request.AddUrlSegment("foo", "bar"); - var client = new RestClient("http://example.com"); - - var expected = new Uri("http://example.com/resource/bar"); - var output = client.BuildUri(request); - - Assert.Equal(expected, output); - } - - [Fact] - public void GET_with_empty_request() - { - var request = new RestRequest(); - var client = new RestClient("http://example.com/resource"); - - var expected = new Uri("http://example.com/resource"); - var output = client.BuildUri(request); - - Assert.Equal(expected, output); - } - - [Fact] - public void GET_with_empty_request_and_bare_hostname() - { - var request = new RestRequest(); - var client = new RestClient("http://example.com"); - - var expected = new Uri("http://example.com/"); - var output = client.BuildUri(request); - - Assert.Equal(expected, output); - } - - [Fact] - public void POST_with_querystring_containing_tokens() - { - var request = new RestRequest("resource", Method.POST); - request.AddParameter("foo", "bar", ParameterType.QueryString); - - var client = new RestClient("http://example.com"); - - var expected = new Uri("http://example.com/resource?foo=bar"); - var output = client.BuildUri(request); - - Assert.Equal(expected, output); - } - - [Fact] - public void GET_with_multiple_instances_of_same_key() - { - var request = new RestRequest("v1/people/~/network/updates", Method.GET); - request.AddParameter("type", "STAT"); - request.AddParameter("type", "PICT"); - request.AddParameter("count", "50"); - request.AddParameter("start", "50"); - - var client = new RestClient("http://api.linkedin.com"); - - var expected = new Uri("http://api.linkedin.com/v1/people/~/network/updates?type=STAT&type=PICT&count=50&start=50"); - var output = client.BuildUri(request); - - Assert.Equal(expected, output); - } - } -} diff --git a/RestSharp.Tests/XmlAttributeDeserializerTests.cs b/RestSharp.Tests/XmlAttributeDeserializerTests.cs deleted file mode 100644 index 0cfc0e26b..000000000 --- a/RestSharp.Tests/XmlAttributeDeserializerTests.cs +++ /dev/null @@ -1,958 +0,0 @@ -#region License -// Copyright 2010 John Sheehan -// -// 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. -#endregion - -using System; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Xml.Linq; -using RestSharp.Deserializers; -using Xunit; -using RestSharp.Tests.SampleClasses; -using System.Collections.Generic; - -namespace RestSharp.Tests -{ - public class XmlAttributeDeserializerTests - { - private const string GuidString = "AC1FC4BC-087A-4242-B8EE-C53EBE9887A5"; - private string SampleDataPath = Path.Combine(Environment.CurrentDirectory, "SampleData"); - - private string PathFor(string sampleFile) - { - return Path.Combine(SampleDataPath, sampleFile); - } - - [Fact] - public void Can_Deserialize_Lists_of_Simple_Types() - { - var xmlpath = PathFor("xmllists.xml"); - var doc = XDocument.Load(xmlpath); - - var xml = new XmlAttributeDeserializer(); - var output = xml.Deserialize(new RestResponse() { Content = doc.ToString() }); - - Assert.NotEmpty(output.Names); - Assert.NotEmpty(output.Numbers); - Assert.False(output.Names[0].Length == 0); - Assert.False(output.Numbers.Sum() == 0); - } - - [Fact] - public void Can_Deserialize_To_List_Inheritor_From_Custom_Root_With_Attributes() - { - var xmlpath = PathFor("ListWithAttributes.xml"); - var doc = XDocument.Load(xmlpath); - - var xml = new XmlAttributeDeserializer(); - xml.RootElement = "Calls"; - var output = xml.Deserialize(new RestResponse { Content = doc.ToString() }); - - Assert.Equal(3, output.NumPages); - Assert.NotEmpty(output); - Assert.Equal(2, output.Count); - } - - [Fact] - public void Can_Deserialize_To_Standalone_List_Without_Matching_Class_Case() - { - var xmlpath = PathFor("InlineListSample.xml"); - var doc = XDocument.Load(xmlpath); - - var xml = new XmlAttributeDeserializer(); - var output = xml.Deserialize>(new RestResponse { Content = doc.ToString() }); - - Assert.NotEmpty(output); - Assert.Equal(4, output.Count); - } - - [Fact] - public void Can_Deserialize_To_Standalone_List_With_Matching_Class_Case() - { - var xmlpath = PathFor("InlineListSample.xml"); - var doc = XDocument.Load(xmlpath); - - var xml = new XmlAttributeDeserializer(); - var output = xml.Deserialize>(new RestResponse { Content = doc.ToString() }); - - Assert.NotEmpty(output); - Assert.Equal(4, output.Count); - } - - [Fact] - public void Can_Deserialize_Directly_To_Lists_Off_Root_Element() - { - var xmlpath = PathFor("directlists.xml"); - var doc = XDocument.Load(xmlpath); - - var xml = new XmlAttributeDeserializer(); - var output = xml.Deserialize>(new RestResponse { Content = doc.ToString() }); - - Assert.NotEmpty(output); - Assert.Equal(2, output.Count); - } - - [Fact] - public void Can_Deserialize_Parentless_aka_Inline_List_Items_Without_Matching_Class_Name() - { - var xmlpath = PathFor("InlineListSample.xml"); - var doc = XDocument.Load(xmlpath); - - var xml = new XmlAttributeDeserializer(); - var output = xml.Deserialize(new RestResponse { Content = doc.ToString() }); - - Assert.NotEmpty(output.Images); - Assert.Equal(4, output.Images.Count); - } - - [Fact] - public void Can_Deserialize_Parentless_aka_Inline_List_Items_With_Matching_Class_Name() - { - var xmlpath = PathFor("InlineListSample.xml"); - var doc = XDocument.Load(xmlpath); - - var xml = new XmlAttributeDeserializer(); - var output = xml.Deserialize(new RestResponse { Content = doc.ToString() }); - - Assert.NotEmpty(output.images); - Assert.Equal(4, output.images.Count); - } - - [Fact] - public void Can_Deserialize_Parentless_aka_Inline_List_Items_With_Matching_Class_Name_With_Additional_Property() - { - var xmlpath = PathFor("InlineListSample.xml"); - var doc = XDocument.Load(xmlpath); - - var xml = new XmlAttributeDeserializer(); - var output = xml.Deserialize(new RestResponse { Content = doc.ToString() }); - - Assert.Equal(4, output.Count); - } - - [Fact] - public void Can_Deserialize_Nested_List_Items_Without_Matching_Class_Name() - { - var xmlpath = PathFor("NestedListSample.xml"); - var doc = XDocument.Load(xmlpath); - - var xml = new XmlAttributeDeserializer(); - var output = xml.Deserialize(new RestResponse { Content = doc.ToString() }); - - Assert.NotEmpty(output.Images); - Assert.Equal(4, output.Images.Count); - } - - - [Fact] - public void Can_Deserialize_Nested_List_Items_With_Matching_Class_Name() - { - var xmlpath = PathFor("NestedListSample.xml"); - var doc = XDocument.Load(xmlpath); - - var xml = new XmlAttributeDeserializer(); - var output = xml.Deserialize(new RestResponse { Content = doc.ToString() }); - - Assert.NotEmpty(output.images); - Assert.Equal(4, output.images.Count); - } - - [Fact] - public void Can_Deserialize_Nested_List_Without_Elements_To_Empty_List() - { - var doc = CreateXmlWithEmptyNestedList(); - - var xml = new XmlAttributeDeserializer(); - var output = xml.Deserialize(new RestResponse { Content = doc }); - - Assert.NotNull(output.images); - Assert.NotNull(output.Images); - Assert.Empty(output.images); - Assert.Empty(output.Images); - } - - [Fact] - public void Can_Deserialize_Inline_List_Without_Elements_To_Empty_List() - { - var doc = CreateXmlWithEmptyInlineList(); - - var xml = new XmlAttributeDeserializer(); - var output = xml.Deserialize(new RestResponse { Content = doc }); - - Assert.NotNull(output.images); - Assert.NotNull(output.Images); - Assert.Empty(output.images); - Assert.Empty(output.Images); - } - - [Fact] - public void Can_Deserialize_Empty_Elements_to_Nullable_Values() - { - var doc = CreateXmlWithNullValues(); - - var xml = new XmlAttributeDeserializer(); - var output = xml.Deserialize(new RestResponse { Content = doc }); - - Assert.Null(output.Id); - Assert.Null(output.StartDate); - Assert.Null(output.UniqueId); - } - - [Fact] - public void Can_Deserialize_Elements_to_Nullable_Values() - { - var culture = CultureInfo.InvariantCulture; - var doc = CreateXmlWithoutEmptyValues(culture); - var xml = new XmlAttributeDeserializer() {Culture = culture}; - var output = xml.Deserialize(new RestResponse { Content = doc }); - - Assert.NotNull(output.Id); - Assert.NotNull(output.StartDate); - Assert.NotNull(output.UniqueId); - - Assert.Equal(123, output.Id); - Assert.Equal(new DateTime(2010, 2, 21, 9, 35, 00), output.StartDate); - Assert.Equal(new Guid(GuidString), output.UniqueId); - } - - [Fact] - public void Can_Deserialize_TimeSpan() - { - var culture = CultureInfo.InvariantCulture; - var doc = new XDocument(culture); - - TimeSpan? nullTimespan = null; - TimeSpan? nullValueTimeSpan = new TimeSpan(21, 30, 7); - - var root = new XElement("Person"); - root.Add(new XElement("Tick", new TimeSpan(468006))); - root.Add(new XElement("Millisecond", new TimeSpan(0, 0, 0, 0, 125))); - root.Add(new XElement("Second", new TimeSpan(0, 0, 8))); - root.Add(new XElement("Minute", new TimeSpan(0, 55, 2))); - root.Add(new XElement("Hour", new TimeSpan(21, 30, 7))); - root.Add(new XElement("NullableWithoutValue", nullTimespan)); - root.Add(new XElement("NullableWithValue", nullValueTimeSpan)); - - doc.Add(root); - - var response = new RestResponse { Content = doc.ToString() }; - - var d = new XmlAttributeDeserializer() - { - Culture = culture, - }; - var payload = d.Deserialize(response); - Assert.Equal(new TimeSpan(468006), payload.Tick); - Assert.Equal(new TimeSpan(0, 0, 0, 0, 125), payload.Millisecond); - Assert.Equal(new TimeSpan(0, 0, 8), payload.Second); - Assert.Equal(new TimeSpan(0, 55, 2), payload.Minute); - Assert.Equal(new TimeSpan(21, 30, 7), payload.Hour); - Assert.Null(payload.NullableWithoutValue); - Assert.NotNull(payload.NullableWithValue); - Assert.Equal(new TimeSpan(21, 30, 7), payload.NullableWithValue.Value); - } - - [Fact] - public void Can_Deserialize_Custom_Formatted_Date() - { - var culture = CultureInfo.InvariantCulture; - var format = "dd yyyy MMM, hh:mm ss tt zzz"; - var date = new DateTime(2010, 2, 8, 11, 11, 11); - - var doc = new XDocument(); - - var root = new XElement("Person"); - root.Add(new XElement("StartDate", date.ToString(format, culture))); - - doc.Add(root); - - var xml = new XmlAttributeDeserializer - { - DateFormat = format, - Culture = culture - }; - - var response = new RestResponse { Content = doc.ToString() }; - var output = xml.Deserialize(response); - - Assert.Equal(date, output.StartDate); - } - - [Fact] - public void Can_Deserialize_Elements_On_Default_Root() - { - var doc = CreateElementsXml(); - var response = new RestResponse { Content = doc }; - - var d = new XmlAttributeDeserializer(); - var p = d.Deserialize(response); - - Assert.Equal("John Sheehan", p.Name); - Assert.Equal(new DateTime(2009, 9, 25, 0, 6, 1), p.StartDate); - Assert.Equal(28, p.Age); - Assert.Equal(long.MaxValue, p.BigNumber); - Assert.Equal(99.9999m, p.Percent); - Assert.Equal(false, p.IsCool); - - Assert.Equal(new Guid(GuidString), p.UniqueId); - Assert.Equal(Guid.Empty, p.EmptyGuid); - - Assert.Equal(new Uri("http://example.com", UriKind.RelativeOrAbsolute), p.Url); - Assert.Equal(new Uri("/foo/bar", UriKind.RelativeOrAbsolute), p.UrlPath); - - Assert.Equal(Order.Third, p.Order); - Assert.Equal(Disposition.SoSo, p.Disposition); - - Assert.NotNull(p.Friends); - Assert.Equal(10, p.Friends.Count); - - Assert.NotNull(p.BestFriend); - Assert.Equal("The Fonz", p.BestFriend.Name); - Assert.Equal(1952, p.BestFriend.Since); - } - - [Fact] - public void Can_Deserialize_Attributes_On_Default_Root() - { - var doc = CreateAttributesXml(); - var response = new RestResponse { Content = doc }; - - var d = new XmlAttributeDeserializer(); - var p = d.Deserialize(response); - - Assert.Equal("John Sheehan", p.Name); - Assert.Equal(new DateTime(2009, 9, 25, 0, 6, 1), p.StartDate); - Assert.Equal(28, p.Age); - Assert.Equal(long.MaxValue, p.BigNumber); - Assert.Equal(99.9999m, p.Percent); - Assert.Equal(false, p.IsCool); - Assert.Equal(new Guid(GuidString), p.UniqueId); - Assert.Equal(new Uri("http://example.com", UriKind.RelativeOrAbsolute), p.Url); - Assert.Equal(new Uri("/foo/bar", UriKind.RelativeOrAbsolute), p.UrlPath); - - Assert.NotNull(p.BestFriend); - Assert.Equal("The Fonz", p.BestFriend.Name); - Assert.Equal(1952, p.BestFriend.Since); - } - - [Fact] - public void Ignore_Protected_Property_That_Exists_In_Data() - { - var doc = CreateElementsXml(); - var response = new RestResponse { Content = doc }; - - var d = new XmlAttributeDeserializer(); - var p = d.Deserialize(response); - - Assert.Null(p.IgnoreProxy); - } - - [Fact] - public void Ignore_ReadOnly_Property_That_Exists_In_Data() - { - var doc = CreateElementsXml(); - var response = new RestResponse { Content = doc }; - - var d = new XmlAttributeDeserializer(); - var p = d.Deserialize(response); - - Assert.Null(p.ReadOnlyProxy); - } - - [Fact] - public void Can_Deserialize_Names_With_Underscores_On_Default_Root() - { - var doc = CreateUnderscoresXml(); - var response = new RestResponse { Content = doc }; - - var d = new XmlAttributeDeserializer(); - var p = d.Deserialize(response); - - Assert.Equal("John Sheehan", p.Name); - Assert.Equal(new DateTime(2009, 9, 25, 0, 6, 1), p.StartDate); - Assert.Equal(28, p.Age); - Assert.Equal(long.MaxValue, p.BigNumber); - Assert.Equal(99.9999m, p.Percent); - Assert.Equal(false, p.IsCool); - Assert.Equal(new Guid(GuidString), p.UniqueId); - Assert.Equal(new Uri("http://example.com", UriKind.RelativeOrAbsolute), p.Url); - Assert.Equal(new Uri("/foo/bar", UriKind.RelativeOrAbsolute), p.UrlPath); - - Assert.NotNull(p.Friends); - Assert.Equal(10, p.Friends.Count); - - Assert.NotNull(p.BestFriend); - Assert.Equal("The Fonz", p.BestFriend.Name); - Assert.Equal(1952, p.BestFriend.Since); - - Assert.NotNull(p.Foes); - Assert.Equal(5, p.Foes.Count); - Assert.Equal("Yankees", p.Foes.Team); - } - - - [Fact] - public void Can_Deserialize_Names_With_Dashes_On_Default_Root() - { - var doc = CreateDashesXml(); - var response = new RestResponse { Content = doc }; - - var d = new XmlAttributeDeserializer(); - var p = d.Deserialize(response); - - Assert.Equal("John Sheehan", p.Name); - Assert.Equal(new DateTime(2009, 9, 25, 0, 6, 1), p.StartDate); - Assert.Equal(28, p.Age); - Assert.Equal(long.MaxValue, p.BigNumber); - Assert.Equal(99.9999m, p.Percent); - Assert.Equal(false, p.IsCool); - Assert.Equal(new Guid(GuidString), p.UniqueId); - Assert.Equal(new Uri("http://example.com", UriKind.RelativeOrAbsolute), p.Url); - Assert.Equal(new Uri("/foo/bar", UriKind.RelativeOrAbsolute), p.UrlPath); - - Assert.NotNull(p.Friends); - Assert.Equal(10, p.Friends.Count); - - Assert.NotNull(p.BestFriend); - Assert.Equal("The Fonz", p.BestFriend.Name); - Assert.Equal(1952, p.BestFriend.Since); - - Assert.NotNull(p.Foes); - Assert.Equal(5, p.Foes.Count); - Assert.Equal("Yankees", p.Foes.Team); - } - - [Fact] - public void Can_Deserialize_Names_With_Underscores_Without_Matching_Case_On_Default_Root () - { - var doc = CreateLowercaseUnderscoresXml (); - var response = new RestResponse { Content = doc }; - - var d = new XmlAttributeDeserializer (); - var p = d.Deserialize (response); - - Assert.Equal ("John Sheehan", p.Name); - Assert.Equal (new DateTime (2009, 9, 25, 0, 6, 1), p.StartDate); - Assert.Equal (28, p.Age); - Assert.Equal (long.MaxValue, p.BigNumber); - Assert.Equal (99.9999m, p.Percent); - Assert.Equal (false, p.IsCool); - Assert.Equal (new Guid (GuidString), p.UniqueId); - Assert.Equal (new Uri ("http://example.com", UriKind.RelativeOrAbsolute), p.Url); - Assert.Equal (new Uri ("/foo/bar", UriKind.RelativeOrAbsolute), p.UrlPath); - - Assert.NotNull (p.Friends); - Assert.Equal (10, p.Friends.Count); - - Assert.NotNull (p.BestFriend); - Assert.Equal ("The Fonz", p.BestFriend.Name); - Assert.Equal (1952, p.BestFriend.Since); - - Assert.NotNull (p.Foes); - Assert.Equal (5, p.Foes.Count); - Assert.Equal ("Yankees", p.Foes.Team); - } - - [Fact] - public void Can_Deserialize_Lower_Cased_Root_Elements_With_Dashes() - { - var doc = CreateDashesXml(); - var response = new RestResponse { Content = doc }; - - var d = new XmlAttributeDeserializer(); - var p = d.Deserialize(response); - - Assert.Equal("John Sheehan", p.Name); - Assert.Equal(new DateTime(2009, 9, 25, 0, 6, 1), p.StartDate); - Assert.Equal(28, p.Age); - Assert.Equal(long.MaxValue, p.BigNumber); - Assert.Equal(99.9999m, p.Percent); - Assert.Equal(false, p.IsCool); - Assert.Equal(new Guid(GuidString), p.UniqueId); - Assert.Equal(new Uri("http://example.com", UriKind.RelativeOrAbsolute), p.Url); - Assert.Equal(new Uri("/foo/bar", UriKind.RelativeOrAbsolute), p.UrlPath); - - Assert.NotNull(p.Friends); - Assert.Equal(10, p.Friends.Count); - - Assert.NotNull(p.BestFriend); - Assert.Equal("The Fonz", p.BestFriend.Name); - Assert.Equal(1952, p.BestFriend.Since); - - Assert.NotNull(p.Foes); - Assert.Equal(5, p.Foes.Count); - Assert.Equal("Yankees", p.Foes.Team); - } - - [Fact] - public void Can_Deserialize_Root_Elements_Without_Matching_Case_And_Dashes() - { - var doc = CreateLowerCasedRootElementWithDashesXml(); - var response = new RestResponse { Content = doc }; - - var d = new XmlAttributeDeserializer(); - var p = d.Deserialize>(response); - - Assert.NotNull(p); - Assert.Equal(1, p.Count); - Assert.Equal(45, p[0].ConceptId); - } - - - [Fact] - public void Can_Deserialize_Eventful_Xml() - { - var xmlpath = PathFor("eventful.xml"); - var doc = XDocument.Load(xmlpath); - var response = new RestResponse { Content = doc.ToString() }; - - var d = new XmlAttributeDeserializer(); - var output = d.Deserialize(response); - - Assert.NotEmpty(output.venues); - Assert.Equal(3, output.venues.Count); - Assert.Equal("Tivoli", output.venues[0].name); - Assert.Equal("http://eventful.com/brisbane/venues/tivoli-/V0-001-002169294-8", output.venues[1].url); - Assert.Equal("V0-001-000266914-3", output.venues[2].id); - } - - [Fact] - public void Can_Deserialize_Lastfm_Xml() - { - var xmlpath = PathFor("Lastfm.xml"); - var doc = XDocument.Load(xmlpath); - var response = new RestResponse { Content = doc.ToString() }; - - var d = new XmlAttributeDeserializer(); - var output = d.Deserialize(response); - - //Assert.NotEmpty(output.artists); - Assert.Equal("http://www.last.fm/event/328799+Philip+Glass+at+Barbican+Centre+on+12+June+2008", output.url); - Assert.Equal("http://www.last.fm/venue/8777860+Barbican+Centre", output.venue.url); - } - - [Fact] - public void Can_Deserialize_Google_Weather_Xml() - { - var xmlpath = PathFor("GoogleWeather.xml"); - var doc = XDocument.Load(xmlpath); - var response = new RestResponse { Content = doc.ToString() }; - - var d = new XmlAttributeDeserializer(); - var output = d.Deserialize(response); - - Assert.NotEmpty(output.weather); - Assert.Equal(4, output.weather.Count); - Assert.Equal("Sunny", output.weather[0].condition.data); - } - - [Fact] - public void Can_Deserialize_Google_Weather_Xml_WithDeserializeAs() - { - var xmlpath = PathFor("GoogleWeather.xml"); - var doc = XDocument.Load(xmlpath); - var response = new RestResponse { Content = doc.ToString() }; - - var d = new XmlAttributeDeserializer(); - var output = d.Deserialize(response); - - Assert.NotEmpty(output.Weather); - Assert.Equal(4, output.Weather.Count); - Assert.Equal("Sunny", output.Weather[0].Condition.Data); - } - - [Fact] - public void Can_Deserialize_Boolean_From_Number() - { - var xmlpath = PathFor("boolean_from_number.xml"); - var doc = XDocument.Load(xmlpath); - var response = new RestResponse { Content = doc.ToString() }; - - var d = new XmlAttributeDeserializer(); - var output = d.Deserialize(response); - - Assert.True(output.Value); - } - - [Fact] - public void Can_Deserialize_Boolean_From_String() - { - var xmlpath = PathFor("boolean_from_string.xml"); - var doc = XDocument.Load(xmlpath); - var response = new RestResponse { Content = doc.ToString() }; - - var d = new XmlAttributeDeserializer(); - var output = d.Deserialize(response); - - Assert.True(output.Value); - } - - [Fact] - public void Can_Deserialize_Empty_Elements_With_Attributes_to_Nullable_Values() - { - var doc = CreateXmlWithAttributesAndNullValues(); - - var xml = new XmlAttributeDeserializer(); - var output = xml.Deserialize(new RestResponse {Content = doc}); - - Assert.Null(output.Id); - Assert.Null(output.StartDate); - Assert.Null(output.UniqueId); - } - - [Fact] - public void Can_Deserialize_Mixture_Of_Empty_Elements_With_Attributes_And_Populated_Elements() - { - var doc = CreateXmlWithAttributesAndNullValuesAndPopulatedValues(); - - var xml = new XmlAttributeDeserializer(); - var output = xml.Deserialize(new RestResponse {Content = doc}); - - Assert.Null(output.Id); - Assert.Null(output.StartDate); - Assert.Equal(new Guid(GuidString), output.UniqueId); - } - - [Fact] - public void Can_Deserialize_DateTimeOffset() - { - var culture = CultureInfo.InvariantCulture; - var doc = new XDocument(culture); - - DateTimeOffset DateTimeOffset = new DateTimeOffset(2013, 02, 08, 9, 18, 22, TimeSpan.FromHours(10)); - DateTimeOffset? NullableDateTimeOffsetWithValue = new DateTimeOffset(2013, 02, 08, 9, 18, 23, TimeSpan.FromHours(10)); - - var root = new XElement("Dates"); - root.Add(new XElement("DateTimeOffset", DateTimeOffset)); - root.Add(new XElement("NullableDateTimeOffsetWithNull", string.Empty)); - root.Add(new XElement("NullableDateTimeOffsetWithValue", NullableDateTimeOffsetWithValue)); - - doc.Add(root); - - var xml = new XmlAttributeDeserializer - { - Culture = culture, - }; - - var response = new RestResponse { Content = doc.ToString() }; - - var d = new XmlAttributeDeserializer() - { - Culture = culture, - }; - var payload = d.Deserialize(response); - Assert.Equal(DateTimeOffset, payload.DateTimeOffset); - Assert.Null(payload.NullableDateTimeOffsetWithNull); - - Assert.True(payload.NullableDateTimeOffsetWithValue.HasValue); - Assert.Equal(NullableDateTimeOffsetWithValue, payload.NullableDateTimeOffsetWithValue); - } - - private static string CreateUnderscoresXml() - { - var doc = new XDocument(); - var root = new XElement("Person"); - root.Add(new XElement("Name", "John Sheehan")); - root.Add(new XElement("Start_Date", new DateTime(2009, 9, 25, 0, 6, 1))); - root.Add(new XAttribute("Age", 28)); - root.Add(new XElement("Percent", 99.9999m)); - root.Add(new XElement("Big_Number", long.MaxValue)); - root.Add(new XAttribute("Is_Cool", false)); - root.Add(new XElement("Ignore", "dummy")); - root.Add(new XAttribute("Read_Only", "dummy")); - root.Add(new XElement("Unique_Id", new Guid(GuidString))); - root.Add(new XElement("Url", "http://example.com")); - root.Add(new XElement("Url_Path", "/foo/bar")); - - root.Add(new XElement("Best_Friend", - new XElement("Name", "The Fonz"), - new XAttribute("Since", 1952) - )); - - var friends = new XElement("Friends"); - for (int i = 0; i < 10; i++) - { - friends.Add(new XElement("Friend", - new XElement("Name", "Friend" + i), - new XAttribute("Since", DateTime.Now.Year - i) - )); - } - root.Add(friends); - - var foes = new XElement("Foes"); - foes.Add(new XAttribute("Team", "Yankees")); - for (int i = 0; i < 5; i++) - { - foes.Add(new XElement("Foe", new XElement("Nickname", "Foe" + i))); - } - root.Add(foes); - - doc.Add(root); - return doc.ToString(); - } - - private static string CreateLowercaseUnderscoresXml() - { - var doc = new XDocument(); - var root = new XElement("Person"); - root.Add(new XElement("Name", "John Sheehan")); - root.Add(new XElement("start_date", new DateTime(2009, 9, 25, 0, 6, 1))); - root.Add(new XAttribute("Age", 28)); - root.Add(new XElement("Percent", 99.9999m)); - root.Add(new XElement("big_number", long.MaxValue)); - root.Add(new XAttribute("is_cool", false)); - root.Add(new XElement("Ignore", "dummy")); - root.Add(new XAttribute("read_only", "dummy")); - root.Add(new XElement("unique_id", new Guid(GuidString))); - root.Add(new XElement("Url", "http://example.com")); - root.Add(new XElement("url_path", "/foo/bar")); - - root.Add(new XElement("best_friend", - new XElement("name", "The Fonz"), - new XAttribute("Since", 1952) - )); - - var friends = new XElement("Friends"); - for (int i = 0; i < 10; i++) - { - friends.Add(new XElement("Friend", - new XElement("Name", "Friend" + i), - new XAttribute("Since", DateTime.Now.Year - i) - )); - } - root.Add(friends); - - var foes = new XElement("Foes"); - foes.Add(new XAttribute("Team", "Yankees")); - for (int i = 0; i < 5; i++) - { - foes.Add(new XElement("Foe", new XElement("Nickname", "Foe" + i))); - } - root.Add(foes); - - doc.Add(root); - - return doc.ToString(); - } - - private static string CreateDashesXml() - { - var doc = new XDocument(); - var root = new XElement("Person"); - root.Add(new XElement("Name", "John Sheehan")); - root.Add(new XElement("Start_Date", new DateTime(2009, 9, 25, 0, 6, 1))); - root.Add(new XAttribute("Age", 28)); - root.Add(new XElement("Percent", 99.9999m)); - root.Add(new XElement("Big-Number", long.MaxValue)); - root.Add(new XAttribute("Is-Cool", false)); - root.Add(new XElement("Ignore", "dummy")); - root.Add(new XAttribute("Read-Only", "dummy")); - root.Add(new XElement("Unique-Id", new Guid(GuidString))); - root.Add(new XElement("Url", "http://example.com")); - root.Add(new XElement("Url-Path", "/foo/bar")); - - root.Add(new XElement("Best-Friend", - new XElement("Name", "The Fonz"), - new XAttribute("Since", 1952) - )); - - var friends = new XElement("Friends"); - for (int i = 0; i < 10; i++) - { - friends.Add(new XElement("Friend", - new XElement("Name", "Friend" + i), - new XAttribute("Since", DateTime.Now.Year - i) - )); - } - root.Add(friends); - - var foes = new XElement("Foes"); - foes.Add(new XAttribute("Team", "Yankees")); - for (int i = 0; i < 5; i++) - { - foes.Add(new XElement("Foe", new XElement("Nickname", "Foe" + i))); - } - root.Add(foes); - - doc.Add(root); - return doc.ToString(); - } - - private static string CreateLowerCasedRootElementWithDashesXml() - { - var doc = new XDocument(); - var root = new XElement("incoming-invoices", - new XElement("incoming-invoice", - new XElement("concept-id", 45) - ) - ); - doc.Add(root); - return doc.ToString(); - } - - private static string CreateElementsXml() - { - var doc = new XDocument(); - var root = new XElement("Person"); - root.Add(new XElement("Name", "John Sheehan")); - root.Add(new XElement("StartDate", new DateTime(2009, 9, 25, 0, 6, 1))); - root.Add(new XElement("Age", 28)); - root.Add(new XElement("Percent", 99.9999m)); - root.Add(new XElement("BigNumber", long.MaxValue)); - root.Add(new XElement("IsCool", false)); - root.Add(new XElement("Ignore", "dummy")); - root.Add(new XElement("ReadOnly", "dummy")); - - root.Add(new XElement("UniqueId", new Guid(GuidString))); - root.Add(new XElement("EmptyGuid", "")); - - root.Add(new XElement("Url", "http://example.com")); - root.Add(new XElement("UrlPath", "/foo/bar")); - root.Add(new XElement("Order", "third")); - root.Add(new XElement("Disposition", "so-so")); - - root.Add(new XElement("BestFriend", - new XElement("Name", "The Fonz"), - new XElement("Since", 1952) - )); - - var friends = new XElement("Friends"); - for (int i = 0; i < 10; i++) - { - friends.Add(new XElement("Friend", - new XElement("Name", "Friend" + i), - new XElement("Since", DateTime.Now.Year - i) - )); - } - root.Add(friends); - - doc.Add(root); - return doc.ToString(); - } - - private static string CreateAttributesXml() - { - var doc = new XDocument(); - var root = new XElement("Person"); - root.Add(new XAttribute("Name", "John Sheehan")); - root.Add(new XAttribute("StartDate", new DateTime(2009, 9, 25, 0, 6, 1))); - root.Add(new XAttribute("Age", 28)); - root.Add(new XAttribute("Percent", 99.9999m)); - root.Add(new XAttribute("BigNumber", long.MaxValue)); - root.Add(new XAttribute("IsCool", false)); - root.Add(new XAttribute("Ignore", "dummy")); - root.Add(new XAttribute("ReadOnly", "dummy")); - root.Add(new XAttribute("UniqueId", new Guid(GuidString))); - root.Add(new XAttribute("Url", "http://example.com")); - root.Add(new XAttribute("UrlPath", "/foo/bar")); - - root.Add(new XElement("BestFriend", - new XAttribute("Name", "The Fonz"), - new XAttribute("Since", 1952) - )); - - doc.Add(root); - return doc.ToString(); - } - - private static string CreateXmlWithNullValues() - { - var doc = new XDocument(); - var root = new XElement("NullableValues"); - - root.Add(new XElement("Id", null), - new XElement("StartDate", null), - new XElement("UniqueId", null) - ); - - doc.Add(root); - - return doc.ToString(); - } - - private static string CreateXmlWithoutEmptyValues(CultureInfo culture) - { - var doc = new XDocument(); - var root = new XElement("NullableValues"); - - root.Add(new XElement("Id", 123), - new XElement("StartDate", new DateTime(2010, 2, 21, 9, 35, 00).ToString(culture)), - new XElement("UniqueId", new Guid(GuidString)) - ); - - doc.Add(root); - - return doc.ToString(); - } - - private static string CreateXmlWithEmptyNestedList() - { - var doc = new XDocument(); - var root = new XElement("EmptyListSample"); - - root.Add(new XElement("Images")); - - doc.Add(root); - - return doc.ToString(); - } - - private static string CreateXmlWithEmptyInlineList() - { - var doc = new XDocument(); - var root = new XElement("EmptyListSample"); - - doc.Add(root); - - return doc.ToString(); - } - - private static string CreateXmlWithAttributesAndNullValues() - { - var doc = new XDocument(); - var root = new XElement("NullableValues"); - - var idElement = new XElement("Id", null); - idElement.SetAttributeValue("SomeAttribute", "SomeAttribute_Value"); - root.Add(idElement, - new XElement("StartDate", null), - new XElement("UniqueId", null) - ); - - doc.Add(root); - - return doc.ToString(); - } - - private static string CreateXmlWithAttributesAndNullValuesAndPopulatedValues() - { - var doc = new XDocument(); - var root = new XElement("NullableValues"); - - var idElement = new XElement("Id", null); - idElement.SetAttributeValue("SomeAttribute", "SomeAttribute_Value"); - root.Add(idElement, - new XElement("StartDate", null), - new XElement("UniqueId", new Guid(GuidString)) - ); - - doc.Add(root); - - return doc.ToString(); - } - - } -} diff --git a/RestSharp.Tests/XmlDeserializerTests.cs b/RestSharp.Tests/XmlDeserializerTests.cs deleted file mode 100644 index 6deef898d..000000000 --- a/RestSharp.Tests/XmlDeserializerTests.cs +++ /dev/null @@ -1,943 +0,0 @@ -#region License -// Copyright 2010 John Sheehan -// -// 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. -#endregion - -using System; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Xml.Linq; -using RestSharp.Deserializers; -using Xunit; -using RestSharp.Tests.SampleClasses; -using System.Collections.Generic; - -namespace RestSharp.Tests -{ - public class XmlDeserializerTests - { - private const string GuidString = "AC1FC4BC-087A-4242-B8EE-C53EBE9887A5"; - private string SampleDataPath = Path.Combine(Environment.CurrentDirectory, "SampleData"); - - private string PathFor(string sampleFile) - { - return Path.Combine(SampleDataPath, sampleFile); - } - - [Fact] - public void Can_Deserialize_Lists_of_Simple_Types() - { - var xmlpath = PathFor("xmllists.xml"); - var doc = XDocument.Load(xmlpath); - - var xml = new XmlDeserializer(); - var output = xml.Deserialize(new RestResponse() { Content = doc.ToString() }); - - Assert.NotEmpty(output.Names); - Assert.NotEmpty(output.Numbers); - Assert.False(output.Names[0].Length == 0); - Assert.False(output.Numbers.Sum() == 0); - } - - [Fact] - public void Can_Deserialize_To_List_Inheritor_From_Custom_Root_With_Attributes() - { - var xmlpath = PathFor("ListWithAttributes.xml"); - var doc = XDocument.Load(xmlpath); - - var xml = new XmlDeserializer(); - xml.RootElement = "Calls"; - var output = xml.Deserialize(new RestResponse { Content = doc.ToString() }); - - Assert.Equal(3, output.NumPages); - Assert.NotEmpty(output); - Assert.Equal(2, output.Count); - } - - [Fact] - public void Can_Deserialize_To_Standalone_List_Without_Matching_Class_Case() - { - var xmlpath = PathFor("InlineListSample.xml"); - var doc = XDocument.Load(xmlpath); - - var xml = new XmlDeserializer(); - var output = xml.Deserialize>(new RestResponse { Content = doc.ToString() }); - - Assert.NotEmpty(output); - Assert.Equal(4, output.Count); - } - - [Fact] - public void Can_Deserialize_To_Standalone_List_With_Matching_Class_Case() - { - var xmlpath = PathFor("InlineListSample.xml"); - var doc = XDocument.Load(xmlpath); - - var xml = new XmlDeserializer(); - var output = xml.Deserialize>(new RestResponse { Content = doc.ToString() }); - - Assert.NotEmpty(output); - Assert.Equal(4, output.Count); - } - - [Fact] - public void Can_Deserialize_Directly_To_Lists_Off_Root_Element() - { - var xmlpath = PathFor("directlists.xml"); - var doc = XDocument.Load(xmlpath); - - var xml = new XmlDeserializer(); - var output = xml.Deserialize>(new RestResponse { Content = doc.ToString() }); - - Assert.NotEmpty(output); - Assert.Equal(2, output.Count); - } - - [Fact] - public void Can_Deserialize_Parentless_aka_Inline_List_Items_Without_Matching_Class_Name() - { - var xmlpath = PathFor("InlineListSample.xml"); - var doc = XDocument.Load(xmlpath); - - var xml = new XmlDeserializer(); - var output = xml.Deserialize(new RestResponse { Content = doc.ToString() }); - - Assert.NotEmpty(output.Images); - Assert.Equal(4, output.Images.Count); - } - - [Fact] - public void Can_Deserialize_Parentless_aka_Inline_List_Items_With_Matching_Class_Name() - { - var xmlpath = PathFor("InlineListSample.xml"); - var doc = XDocument.Load(xmlpath); - - var xml = new XmlDeserializer(); - var output = xml.Deserialize(new RestResponse { Content = doc.ToString() }); - - Assert.NotEmpty(output.images); - Assert.Equal(4, output.images.Count); - } - - [Fact] - public void Can_Deserialize_Parentless_aka_Inline_List_Items_With_Matching_Class_Name_With_Additional_Property() - { - var xmlpath = PathFor("InlineListSample.xml"); - var doc = XDocument.Load(xmlpath); - - var xml = new XmlDeserializer(); - var output = xml.Deserialize(new RestResponse { Content = doc.ToString() }); - - Assert.Equal(4, output.Count); - } - - [Fact] - public void Can_Deserialize_Nested_List_Items_Without_Matching_Class_Name() - { - var xmlpath = PathFor("NestedListSample.xml"); - var doc = XDocument.Load(xmlpath); - - var xml = new XmlDeserializer(); - var output = xml.Deserialize(new RestResponse { Content = doc.ToString() }); - - Assert.NotEmpty(output.Images); - Assert.Equal(4, output.Images.Count); - } - - - [Fact] - public void Can_Deserialize_Nested_List_Items_With_Matching_Class_Name() - { - var xmlpath = PathFor("NestedListSample.xml"); - var doc = XDocument.Load(xmlpath); - - var xml = new XmlDeserializer(); - var output = xml.Deserialize(new RestResponse { Content = doc.ToString() }); - - Assert.NotEmpty(output.images); - Assert.Equal(4, output.images.Count); - } - - [Fact] - public void Can_Deserialize_Nested_List_Without_Elements_To_Empty_List() - { - var doc = CreateXmlWithEmptyNestedList(); - - var xml = new XmlDeserializer(); - var output = xml.Deserialize(new RestResponse { Content = doc }); - - Assert.NotNull(output.images); - Assert.NotNull(output.Images); - Assert.Empty(output.images); - Assert.Empty(output.Images); - } - - [Fact] - public void Can_Deserialize_Inline_List_Without_Elements_To_Empty_List() - { - var doc = CreateXmlWithEmptyInlineList(); - - var xml = new XmlDeserializer(); - var output = xml.Deserialize(new RestResponse { Content = doc }); - - Assert.NotNull(output.images); - Assert.NotNull(output.Images); - Assert.Empty(output.images); - Assert.Empty(output.Images); - } - - [Fact] - public void Can_Deserialize_Empty_Elements_to_Nullable_Values() - { - var doc = CreateXmlWithNullValues(); - - var xml = new XmlDeserializer(); - var output = xml.Deserialize(new RestResponse { Content = doc }); - - Assert.Null(output.Id); - Assert.Null(output.StartDate); - Assert.Null(output.UniqueId); - } - - [Fact] - public void Can_Deserialize_Elements_to_Nullable_Values() - { - var culture = CultureInfo.InvariantCulture; - var doc = CreateXmlWithoutEmptyValues(culture); - var xml = new XmlDeserializer() {Culture = culture}; - var output = xml.Deserialize(new RestResponse { Content = doc }); - - Assert.NotNull(output.Id); - Assert.NotNull(output.StartDate); - Assert.NotNull(output.UniqueId); - - Assert.Equal(123, output.Id); - Assert.Equal(new DateTime(2010, 2, 21, 9, 35, 00), output.StartDate); - Assert.Equal(new Guid(GuidString), output.UniqueId); - } - - [Fact] - public void Can_Deserialize_TimeSpan() - { - var culture = CultureInfo.InvariantCulture; - var doc = new XDocument(culture); - - TimeSpan? nullTimespan = null; - TimeSpan? nullValueTimeSpan = new TimeSpan(21, 30, 7); - - var root = new XElement("Person"); - root.Add(new XElement("Tick", new TimeSpan(468006))); - root.Add(new XElement("Millisecond", new TimeSpan(0, 0, 0, 0, 125))); - root.Add(new XElement("Second", new TimeSpan(0, 0, 8))); - root.Add(new XElement("Minute", new TimeSpan(0, 55, 2))); - root.Add(new XElement("Hour", new TimeSpan(21, 30, 7))); - root.Add(new XElement("NullableWithoutValue", nullTimespan)); - root.Add(new XElement("NullableWithValue", nullValueTimeSpan)); - - doc.Add(root); - - var response = new RestResponse { Content = doc.ToString() }; - - var d = new XmlDeserializer() - { - Culture = culture, - }; - var payload = d.Deserialize(response); - Assert.Equal(new TimeSpan(468006), payload.Tick); - Assert.Equal(new TimeSpan(0, 0, 0, 0, 125), payload.Millisecond); - Assert.Equal(new TimeSpan(0, 0, 8), payload.Second); - Assert.Equal(new TimeSpan(0, 55, 2), payload.Minute); - Assert.Equal(new TimeSpan(21, 30, 7), payload.Hour); - Assert.Null(payload.NullableWithoutValue); - Assert.NotNull(payload.NullableWithValue); - Assert.Equal(new TimeSpan(21, 30, 7), payload.NullableWithValue.Value); - } - - [Fact] - public void Can_Deserialize_Custom_Formatted_Date() - { - var culture = CultureInfo.InvariantCulture; - var format = "dd yyyy MMM, hh:mm ss tt zzz"; - var date = new DateTime(2010, 2, 8, 11, 11, 11); - - var doc = new XDocument(); - - var root = new XElement("Person"); - root.Add(new XElement("StartDate", date.ToString(format, culture))); - - doc.Add(root); - - var xml = new XmlDeserializer - { - DateFormat = format, - Culture = culture - }; - - var response = new RestResponse { Content = doc.ToString() }; - var output = xml.Deserialize(response); - - Assert.Equal(date, output.StartDate); - } - - [Fact] - public void Can_Deserialize_Elements_On_Default_Root() - { - var doc = CreateElementsXml(); - var response = new RestResponse { Content = doc }; - - var d = new XmlDeserializer(); - var p = d.Deserialize(response); - - Assert.Equal("John Sheehan", p.Name); - Assert.Equal(new DateTime(2009, 9, 25, 0, 6, 1), p.StartDate); - Assert.Equal(28, p.Age); - Assert.Equal(long.MaxValue, p.BigNumber); - Assert.Equal(99.9999m, p.Percent); - Assert.Equal(false, p.IsCool); - - Assert.Equal(new Guid(GuidString), p.UniqueId); - Assert.Equal(Guid.Empty, p.EmptyGuid); - - Assert.Equal(new Uri("http://example.com", UriKind.RelativeOrAbsolute), p.Url); - Assert.Equal(new Uri("/foo/bar", UriKind.RelativeOrAbsolute), p.UrlPath); - - Assert.Equal(Order.Third, p.Order); - Assert.Equal(Disposition.SoSo, p.Disposition); - - Assert.NotNull(p.Friends); - Assert.Equal(10, p.Friends.Count); - - Assert.NotNull(p.BestFriend); - Assert.Equal("The Fonz", p.BestFriend.Name); - Assert.Equal(1952, p.BestFriend.Since); - } - - [Fact] - public void Can_Deserialize_Attributes_On_Default_Root() - { - var doc = CreateAttributesXml(); - var response = new RestResponse { Content = doc }; - - var d = new XmlDeserializer(); - var p = d.Deserialize(response); - - Assert.Equal("John Sheehan", p.Name); - Assert.Equal(new DateTime(2009, 9, 25, 0, 6, 1), p.StartDate); - Assert.Equal(28, p.Age); - Assert.Equal(long.MaxValue, p.BigNumber); - Assert.Equal(99.9999m, p.Percent); - Assert.Equal(false, p.IsCool); - Assert.Equal(new Guid(GuidString), p.UniqueId); - Assert.Equal(new Uri("http://example.com", UriKind.RelativeOrAbsolute), p.Url); - Assert.Equal(new Uri("/foo/bar", UriKind.RelativeOrAbsolute), p.UrlPath); - - Assert.NotNull(p.BestFriend); - Assert.Equal("The Fonz", p.BestFriend.Name); - Assert.Equal(1952, p.BestFriend.Since); - } - - [Fact] - public void Ignore_Protected_Property_That_Exists_In_Data() - { - var doc = CreateElementsXml(); - var response = new RestResponse { Content = doc }; - - var d = new XmlDeserializer(); - var p = d.Deserialize(response); - - Assert.Null(p.IgnoreProxy); - } - - [Fact] - public void Ignore_ReadOnly_Property_That_Exists_In_Data() - { - var doc = CreateElementsXml(); - var response = new RestResponse { Content = doc }; - - var d = new XmlDeserializer(); - var p = d.Deserialize(response); - - Assert.Null(p.ReadOnlyProxy); - } - - [Fact] - public void Can_Deserialize_Names_With_Underscores_On_Default_Root() - { - var doc = CreateUnderscoresXml(); - var response = new RestResponse { Content = doc }; - - var d = new XmlDeserializer(); - var p = d.Deserialize(response); - - Assert.Equal("John Sheehan", p.Name); - Assert.Equal(new DateTime(2009, 9, 25, 0, 6, 1), p.StartDate); - Assert.Equal(28, p.Age); - Assert.Equal(long.MaxValue, p.BigNumber); - Assert.Equal(99.9999m, p.Percent); - Assert.Equal(false, p.IsCool); - Assert.Equal(new Guid(GuidString), p.UniqueId); - Assert.Equal(new Uri("http://example.com", UriKind.RelativeOrAbsolute), p.Url); - Assert.Equal(new Uri("/foo/bar", UriKind.RelativeOrAbsolute), p.UrlPath); - - Assert.NotNull(p.Friends); - Assert.Equal(10, p.Friends.Count); - - Assert.NotNull(p.BestFriend); - Assert.Equal("The Fonz", p.BestFriend.Name); - Assert.Equal(1952, p.BestFriend.Since); - - Assert.NotNull(p.Foes); - Assert.Equal(5, p.Foes.Count); - Assert.Equal("Yankees", p.Foes.Team); - } - - - [Fact] - public void Can_Deserialize_Names_With_Dashes_On_Default_Root() - { - var doc = CreateDashesXml(); - var response = new RestResponse { Content = doc }; - - var d = new XmlDeserializer(); - var p = d.Deserialize(response); - - Assert.Equal("John Sheehan", p.Name); - Assert.Equal(new DateTime(2009, 9, 25, 0, 6, 1), p.StartDate); - Assert.Equal(28, p.Age); - Assert.Equal(long.MaxValue, p.BigNumber); - Assert.Equal(99.9999m, p.Percent); - Assert.Equal(false, p.IsCool); - Assert.Equal(new Guid(GuidString), p.UniqueId); - Assert.Equal(new Uri("http://example.com", UriKind.RelativeOrAbsolute), p.Url); - Assert.Equal(new Uri("/foo/bar", UriKind.RelativeOrAbsolute), p.UrlPath); - - Assert.NotNull(p.Friends); - Assert.Equal(10, p.Friends.Count); - - Assert.NotNull(p.BestFriend); - Assert.Equal("The Fonz", p.BestFriend.Name); - Assert.Equal(1952, p.BestFriend.Since); - - Assert.NotNull(p.Foes); - Assert.Equal(5, p.Foes.Count); - Assert.Equal("Yankees", p.Foes.Team); - } - - [Fact] - public void Can_Deserialize_Names_With_Underscores_Without_Matching_Case_On_Default_Root () - { - var doc = CreateLowercaseUnderscoresXml (); - var response = new RestResponse { Content = doc }; - - var d = new XmlDeserializer (); - var p = d.Deserialize (response); - - Assert.Equal ("John Sheehan", p.Name); - Assert.Equal (new DateTime (2009, 9, 25, 0, 6, 1), p.StartDate); - Assert.Equal (28, p.Age); - Assert.Equal (long.MaxValue, p.BigNumber); - Assert.Equal (99.9999m, p.Percent); - Assert.Equal (false, p.IsCool); - Assert.Equal (new Guid (GuidString), p.UniqueId); - Assert.Equal (new Uri ("http://example.com", UriKind.RelativeOrAbsolute), p.Url); - Assert.Equal (new Uri ("/foo/bar", UriKind.RelativeOrAbsolute), p.UrlPath); - - Assert.NotNull (p.Friends); - Assert.Equal (10, p.Friends.Count); - - Assert.NotNull (p.BestFriend); - Assert.Equal ("The Fonz", p.BestFriend.Name); - Assert.Equal (1952, p.BestFriend.Since); - - Assert.NotNull (p.Foes); - Assert.Equal (5, p.Foes.Count); - Assert.Equal ("Yankees", p.Foes.Team); - } - - [Fact] - public void Can_Deserialize_Lower_Cased_Root_Elements_With_Dashes() - { - var doc = CreateDashesXml(); - var response = new RestResponse { Content = doc }; - - var d = new XmlDeserializer(); - var p = d.Deserialize(response); - - Assert.Equal("John Sheehan", p.Name); - Assert.Equal(new DateTime(2009, 9, 25, 0, 6, 1), p.StartDate); - Assert.Equal(28, p.Age); - Assert.Equal(long.MaxValue, p.BigNumber); - Assert.Equal(99.9999m, p.Percent); - Assert.Equal(false, p.IsCool); - Assert.Equal(new Guid(GuidString), p.UniqueId); - Assert.Equal(new Uri("http://example.com", UriKind.RelativeOrAbsolute), p.Url); - Assert.Equal(new Uri("/foo/bar", UriKind.RelativeOrAbsolute), p.UrlPath); - - Assert.NotNull(p.Friends); - Assert.Equal(10, p.Friends.Count); - - Assert.NotNull(p.BestFriend); - Assert.Equal("The Fonz", p.BestFriend.Name); - Assert.Equal(1952, p.BestFriend.Since); - - Assert.NotNull(p.Foes); - Assert.Equal(5, p.Foes.Count); - Assert.Equal("Yankees", p.Foes.Team); - } - - [Fact] - public void Can_Deserialize_Root_Elements_Without_Matching_Case_And_Dashes() - { - var doc = CreateLowerCasedRootElementWithDashesXml(); - var response = new RestResponse { Content = doc }; - - var d = new XmlDeserializer(); - var p = d.Deserialize>(response); - - Assert.NotNull(p); - Assert.Equal(1, p.Count); - Assert.Equal(45, p[0].ConceptId); - } - - - [Fact] - public void Can_Deserialize_Eventful_Xml() - { - var xmlpath = PathFor("eventful.xml"); - var doc = XDocument.Load(xmlpath); - var response = new RestResponse { Content = doc.ToString() }; - - var d = new XmlDeserializer(); - var output = d.Deserialize(response); - - Assert.NotEmpty(output.venues); - Assert.Equal(3, output.venues.Count); - Assert.Equal("Tivoli", output.venues[0].name); - Assert.Equal("http://eventful.com/brisbane/venues/tivoli-/V0-001-002169294-8", output.venues[1].url); - Assert.Equal("V0-001-000266914-3", output.venues[2].id); - } - - [Fact] - public void Can_Deserialize_Lastfm_Xml() - { - var xmlpath = PathFor("Lastfm.xml"); - var doc = XDocument.Load(xmlpath); - var response = new RestResponse { Content = doc.ToString() }; - - var d = new XmlDeserializer(); - var output = d.Deserialize(response); - - //Assert.NotEmpty(output.artists); - Assert.Equal("http://www.last.fm/event/328799+Philip+Glass+at+Barbican+Centre+on+12+June+2008", output.url); - Assert.Equal("http://www.last.fm/venue/8777860+Barbican+Centre", output.venue.url); - } - - [Fact] - public void Can_Deserialize_Google_Weather_Xml() - { - var xmlpath = PathFor("GoogleWeather.xml"); - var doc = XDocument.Load(xmlpath); - var response = new RestResponse { Content = doc.ToString() }; - - var d = new XmlDeserializer(); - var output = d.Deserialize(response); - - Assert.NotEmpty(output.weather); - Assert.Equal(4, output.weather.Count); - Assert.Equal("Sunny", output.weather[0].condition.data); - } - - [Fact] - public void Can_Deserialize_Boolean_From_Number() - { - var xmlpath = PathFor("boolean_from_number.xml"); - var doc = XDocument.Load(xmlpath); - var response = new RestResponse { Content = doc.ToString() }; - - var d = new XmlDeserializer(); - var output = d.Deserialize(response); - - Assert.True(output.Value); - } - - [Fact] - public void Can_Deserialize_Boolean_From_String() - { - var xmlpath = PathFor("boolean_from_string.xml"); - var doc = XDocument.Load(xmlpath); - var response = new RestResponse { Content = doc.ToString() }; - - var d = new XmlDeserializer(); - var output = d.Deserialize(response); - - Assert.True(output.Value); - } - - [Fact] - public void Can_Deserialize_Empty_Elements_With_Attributes_to_Nullable_Values() - { - var doc = CreateXmlWithAttributesAndNullValues(); - - var xml = new XmlDeserializer(); - var output = xml.Deserialize(new RestResponse {Content = doc}); - - Assert.Null(output.Id); - Assert.Null(output.StartDate); - Assert.Null(output.UniqueId); - } - - [Fact] - public void Can_Deserialize_Mixture_Of_Empty_Elements_With_Attributes_And_Populated_Elements() - { - var doc = CreateXmlWithAttributesAndNullValuesAndPopulatedValues(); - - var xml = new XmlDeserializer(); - var output = xml.Deserialize(new RestResponse {Content = doc}); - - Assert.Null(output.Id); - Assert.Null(output.StartDate); - Assert.Equal(new Guid(GuidString), output.UniqueId); - } - - [Fact] - public void Can_Deserialize_DateTimeOffset() - { - var culture = CultureInfo.InvariantCulture; - var doc = new XDocument(culture); - - DateTimeOffset DateTimeOffset = new DateTimeOffset(2013, 02, 08, 9, 18, 22, TimeSpan.FromHours(10)); - DateTimeOffset? NullableDateTimeOffsetWithValue = new DateTimeOffset(2013, 02, 08, 9, 18, 23, TimeSpan.FromHours(10)); - - var root = new XElement("Dates"); - root.Add(new XElement("DateTimeOffset", DateTimeOffset)); - root.Add(new XElement("NullableDateTimeOffsetWithNull", string.Empty)); - root.Add(new XElement("NullableDateTimeOffsetWithValue", NullableDateTimeOffsetWithValue)); - - doc.Add(root); - - var xml = new XmlDeserializer - { - Culture = culture, - }; - - var response = new RestResponse { Content = doc.ToString() }; - - var d = new XmlDeserializer() - { - Culture = culture, - }; - var payload = d.Deserialize(response); - Assert.Equal(DateTimeOffset, payload.DateTimeOffset); - Assert.Null(payload.NullableDateTimeOffsetWithNull); - - Assert.True(payload.NullableDateTimeOffsetWithValue.HasValue); - Assert.Equal(NullableDateTimeOffsetWithValue, payload.NullableDateTimeOffsetWithValue); - } - - private static string CreateUnderscoresXml() - { - var doc = new XDocument(); - var root = new XElement("Person"); - root.Add(new XElement("Name", "John Sheehan")); - root.Add(new XElement("Start_Date", new DateTime(2009, 9, 25, 0, 6, 1))); - root.Add(new XAttribute("Age", 28)); - root.Add(new XElement("Percent", 99.9999m)); - root.Add(new XElement("Big_Number", long.MaxValue)); - root.Add(new XAttribute("Is_Cool", false)); - root.Add(new XElement("Ignore", "dummy")); - root.Add(new XAttribute("Read_Only", "dummy")); - root.Add(new XElement("Unique_Id", new Guid(GuidString))); - root.Add(new XElement("Url", "http://example.com")); - root.Add(new XElement("Url_Path", "/foo/bar")); - - root.Add(new XElement("Best_Friend", - new XElement("Name", "The Fonz"), - new XAttribute("Since", 1952) - )); - - var friends = new XElement("Friends"); - for (int i = 0; i < 10; i++) - { - friends.Add(new XElement("Friend", - new XElement("Name", "Friend" + i), - new XAttribute("Since", DateTime.Now.Year - i) - )); - } - root.Add(friends); - - var foes = new XElement("Foes"); - foes.Add(new XAttribute("Team", "Yankees")); - for (int i = 0; i < 5; i++) - { - foes.Add(new XElement("Foe", new XElement("Nickname", "Foe" + i))); - } - root.Add(foes); - - doc.Add(root); - return doc.ToString(); - } - - private static string CreateLowercaseUnderscoresXml() - { - var doc = new XDocument(); - var root = new XElement("Person"); - root.Add(new XElement("Name", "John Sheehan")); - root.Add(new XElement("start_date", new DateTime(2009, 9, 25, 0, 6, 1))); - root.Add(new XAttribute("Age", 28)); - root.Add(new XElement("Percent", 99.9999m)); - root.Add(new XElement("big_number", long.MaxValue)); - root.Add(new XAttribute("is_cool", false)); - root.Add(new XElement("Ignore", "dummy")); - root.Add(new XAttribute("read_only", "dummy")); - root.Add(new XElement("unique_id", new Guid(GuidString))); - root.Add(new XElement("Url", "http://example.com")); - root.Add(new XElement("url_path", "/foo/bar")); - - root.Add(new XElement("best_friend", - new XElement("name", "The Fonz"), - new XAttribute("Since", 1952) - )); - - var friends = new XElement("Friends"); - for (int i = 0; i < 10; i++) - { - friends.Add(new XElement("Friend", - new XElement("Name", "Friend" + i), - new XAttribute("Since", DateTime.Now.Year - i) - )); - } - root.Add(friends); - - var foes = new XElement("Foes"); - foes.Add(new XAttribute("Team", "Yankees")); - for (int i = 0; i < 5; i++) - { - foes.Add(new XElement("Foe", new XElement("Nickname", "Foe" + i))); - } - root.Add(foes); - - doc.Add(root); - - return doc.ToString(); - } - - private static string CreateDashesXml() - { - var doc = new XDocument(); - var root = new XElement("Person"); - root.Add(new XElement("Name", "John Sheehan")); - root.Add(new XElement("Start_Date", new DateTime(2009, 9, 25, 0, 6, 1))); - root.Add(new XAttribute("Age", 28)); - root.Add(new XElement("Percent", 99.9999m)); - root.Add(new XElement("Big-Number", long.MaxValue)); - root.Add(new XAttribute("Is-Cool", false)); - root.Add(new XElement("Ignore", "dummy")); - root.Add(new XAttribute("Read-Only", "dummy")); - root.Add(new XElement("Unique-Id", new Guid(GuidString))); - root.Add(new XElement("Url", "http://example.com")); - root.Add(new XElement("Url-Path", "/foo/bar")); - - root.Add(new XElement("Best-Friend", - new XElement("Name", "The Fonz"), - new XAttribute("Since", 1952) - )); - - var friends = new XElement("Friends"); - for (int i = 0; i < 10; i++) - { - friends.Add(new XElement("Friend", - new XElement("Name", "Friend" + i), - new XAttribute("Since", DateTime.Now.Year - i) - )); - } - root.Add(friends); - - var foes = new XElement("Foes"); - foes.Add(new XAttribute("Team", "Yankees")); - for (int i = 0; i < 5; i++) - { - foes.Add(new XElement("Foe", new XElement("Nickname", "Foe" + i))); - } - root.Add(foes); - - doc.Add(root); - return doc.ToString(); - } - - private static string CreateLowerCasedRootElementWithDashesXml() - { - var doc = new XDocument(); - var root = new XElement("incoming-invoices", - new XElement("incoming-invoice", - new XElement("concept-id", 45) - ) - ); - doc.Add(root); - return doc.ToString(); - } - - private static string CreateElementsXml() - { - var doc = new XDocument(); - var root = new XElement("Person"); - root.Add(new XElement("Name", "John Sheehan")); - root.Add(new XElement("StartDate", new DateTime(2009, 9, 25, 0, 6, 1))); - root.Add(new XElement("Age", 28)); - root.Add(new XElement("Percent", 99.9999m)); - root.Add(new XElement("BigNumber", long.MaxValue)); - root.Add(new XElement("IsCool", false)); - root.Add(new XElement("Ignore", "dummy")); - root.Add(new XElement("ReadOnly", "dummy")); - - root.Add(new XElement("UniqueId", new Guid(GuidString))); - root.Add(new XElement("EmptyGuid", "")); - - root.Add(new XElement("Url", "http://example.com")); - root.Add(new XElement("UrlPath", "/foo/bar")); - root.Add(new XElement("Order", "third")); - root.Add(new XElement("Disposition", "so-so")); - - root.Add(new XElement("BestFriend", - new XElement("Name", "The Fonz"), - new XElement("Since", 1952) - )); - - var friends = new XElement("Friends"); - for (int i = 0; i < 10; i++) - { - friends.Add(new XElement("Friend", - new XElement("Name", "Friend" + i), - new XElement("Since", DateTime.Now.Year - i) - )); - } - root.Add(friends); - - doc.Add(root); - return doc.ToString(); - } - - private static string CreateAttributesXml() - { - var doc = new XDocument(); - var root = new XElement("Person"); - root.Add(new XAttribute("Name", "John Sheehan")); - root.Add(new XAttribute("StartDate", new DateTime(2009, 9, 25, 0, 6, 1))); - root.Add(new XAttribute("Age", 28)); - root.Add(new XAttribute("Percent", 99.9999m)); - root.Add(new XAttribute("BigNumber", long.MaxValue)); - root.Add(new XAttribute("IsCool", false)); - root.Add(new XAttribute("Ignore", "dummy")); - root.Add(new XAttribute("ReadOnly", "dummy")); - root.Add(new XAttribute("UniqueId", new Guid(GuidString))); - root.Add(new XAttribute("Url", "http://example.com")); - root.Add(new XAttribute("UrlPath", "/foo/bar")); - - root.Add(new XElement("BestFriend", - new XAttribute("Name", "The Fonz"), - new XAttribute("Since", 1952) - )); - - doc.Add(root); - return doc.ToString(); - } - - private static string CreateXmlWithNullValues() - { - var doc = new XDocument(); - var root = new XElement("NullableValues"); - - root.Add(new XElement("Id", null), - new XElement("StartDate", null), - new XElement("UniqueId", null) - ); - - doc.Add(root); - - return doc.ToString(); - } - - private static string CreateXmlWithoutEmptyValues(CultureInfo culture) - { - var doc = new XDocument(); - var root = new XElement("NullableValues"); - - root.Add(new XElement("Id", 123), - new XElement("StartDate", new DateTime(2010, 2, 21, 9, 35, 00).ToString(culture)), - new XElement("UniqueId", new Guid(GuidString)) - ); - - doc.Add(root); - - return doc.ToString(); - } - - private static string CreateXmlWithEmptyNestedList() - { - var doc = new XDocument(); - var root = new XElement("EmptyListSample"); - - root.Add(new XElement("Images")); - - doc.Add(root); - - return doc.ToString(); - } - - private static string CreateXmlWithEmptyInlineList() - { - var doc = new XDocument(); - var root = new XElement("EmptyListSample"); - - doc.Add(root); - - return doc.ToString(); - } - - private static string CreateXmlWithAttributesAndNullValues() - { - var doc = new XDocument(); - var root = new XElement("NullableValues"); - - var idElement = new XElement("Id", null); - idElement.SetAttributeValue("SomeAttribute", "SomeAttribute_Value"); - root.Add(idElement, - new XElement("StartDate", null), - new XElement("UniqueId", null) - ); - - doc.Add(root); - - return doc.ToString(); - } - - private static string CreateXmlWithAttributesAndNullValuesAndPopulatedValues() - { - var doc = new XDocument(); - var root = new XElement("NullableValues"); - - var idElement = new XElement("Id", null); - idElement.SetAttributeValue("SomeAttribute", "SomeAttribute_Value"); - root.Add(idElement, - new XElement("StartDate", null), - new XElement("UniqueId", new Guid(GuidString)) - ); - - doc.Add(root); - - return doc.ToString(); - } - - } -} diff --git a/RestSharp.Tests/packages.config b/RestSharp.Tests/packages.config deleted file mode 100644 index 60eb9e3d2..000000000 --- a/RestSharp.Tests/packages.config +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/RestSharp.WindowsPhone.Mango/Properties/AssemblyInfo.cs b/RestSharp.WindowsPhone.Mango/Properties/AssemblyInfo.cs deleted file mode 100644 index 0319a3712..000000000 --- a/RestSharp.WindowsPhone.Mango/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("RestSharp.WindowsPhone")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("edd38716-2ef8-46c4-8730-c4ed8bc12eed")] \ No newline at end of file diff --git a/RestSharp.WindowsPhone.Mango/RestSharp.WindowsPhone.Mango.csproj b/RestSharp.WindowsPhone.Mango/RestSharp.WindowsPhone.Mango.csproj deleted file mode 100644 index 48766029f..000000000 --- a/RestSharp.WindowsPhone.Mango/RestSharp.WindowsPhone.Mango.csproj +++ /dev/null @@ -1,298 +0,0 @@ - - - - Debug - AnyCPU - 10.0.20506 - 2.0 - {71647E33-714F-4975-8368-71B075045272} - {C089C8C0-30E0-4E22-80C0-CE093F111A43};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} - Library - Properties - RestSharp.WindowsPhone - RestSharp.WindowsPhone - v4.0 - $(TargetFrameworkVersion) - WindowsPhone71 - Silverlight - false - true - true - - - true - full - false - Bin\Debug - TRACE;DEBUG;WINDOWS_PHONE;MANGO - true - true - prompt - 4 - Bin\Debug\RestSharp.WindowsPhone.xml - - - pdbonly - true - Bin\Release - TRACE;WINDOWS_PHONE - true - true - prompt - 4 - Bin\Release\RestSharp.WindowsPhone.xml - - - - - - - - - - - - - - Authenticators\HttpBasicAuthenticator.cs - - - Authenticators\IAuthenticator.cs - - - Authenticators\NtlmAuthenticator.cs - - - Authenticators\OAuth1Authenticator.cs - - - Authenticators\OAuth2Authenticator.cs - - - Authenticators\OAuth\Extensions\CollectionExtensions.cs - - - Authenticators\OAuth\Extensions\OAuthExtensions.cs - - - Authenticators\OAuth\Extensions\StringExtensions.cs - - - Authenticators\OAuth\Extensions\TimeExtensions.cs - - - Authenticators\OAuth\HttpPostParameter.cs - - - Authenticators\OAuth\HttpPostParameterType.cs - - - Authenticators\OAuth\OAuthParameterHandling.cs - - - Authenticators\OAuth\OAuthSignatureMethod.cs - - - Authenticators\OAuth\OAuthSignatureTreatment.cs - - - Authenticators\OAuth\OAuthTools.cs - - - Authenticators\OAuth\OAuthType.cs - - - Authenticators\OAuth\OAuthWebQueryInfo.cs - - - Authenticators\OAuth\OAuthWorkflow.cs - - - Authenticators\OAuth\WebPair.cs - - - Authenticators\OAuth\WebPairCollection.cs - - - Authenticators\OAuth\WebParameter.cs - - - Authenticators\OAuth\WebParameterCollection.cs - - - Authenticators\SimpleAuthenticator.cs - - - Compression\ZLib\Crc32.cs - - - Compression\ZLib\FlushType.cs - - - Compression\ZLib\GZipStream.cs - - - Compression\ZLib\Inflate.cs - - - Compression\ZLib\InfTree.cs - - - Compression\ZLib\ZLib.cs - - - Compression\ZLib\ZLibCodec.cs - - - Compression\ZLib\ZLibConstants.cs - - - Compression\ZLib\ZLibStream.cs - - - Deserializers\DeserializeAsAttribute.cs - - - Deserializers\DotNetXmlDeserializer.cs - - - Deserializers\IDeserializer.cs - - - Deserializers\JsonDeserializer.cs - - - Deserializers\XmlAttributeDeserializer.cs - - - Deserializers\XmlDeserializer.cs - - - Enum.cs - - - Extensions\MiscExtensions.cs - - - Extensions\ReflectionExtensions.cs - - - Extensions\StringExtensions.cs - - - Extensions\ResponseExtensions.cs - - - Extensions\XmlExtensions.cs - - - FileParameter.cs - - - Http.Async.cs - - - Http.cs - - - HttpCookie.cs - - - HttpFile.cs - - - HttpHeader.cs - - - HttpParameter.cs - - - HttpResponse.cs - - - IHttp.cs - - - IHttpFactory.cs - - - IHttpResponse.cs - - - IRestClient.cs - - - IRestRequest.cs - - - IRestResponse.cs - - - Parameter.cs - - - RestClient.Async.cs - - - RestClient.cs - - - RestClientExtensions.cs - - - RestRequest.cs - - - RestResponse.cs - - - RestResponseCookie.cs - - - Serializers\DotNetXmlSerializer.cs - - - Serializers\ISerializer.cs - - - Serializers\JsonSerializer.cs - - - Serializers\SerializeAsAttribute.cs - - - Serializers\XmlSerializer.cs - - - SharedAssemblyInfo.cs - - - SimpleJson.cs - - - Validation\Require.cs - - - Validation\Validate.cs - - - RestRequestAsyncHandle.cs - - - - - - Designer - - - - - - - \ No newline at end of file diff --git a/RestSharp.WindowsPhone.Mango/RestSharp.WindowsPhone.Mango.sln b/RestSharp.WindowsPhone.Mango/RestSharp.WindowsPhone.Mango.sln deleted file mode 100644 index 697bcf545..000000000 --- a/RestSharp.WindowsPhone.Mango/RestSharp.WindowsPhone.Mango.sln +++ /dev/null @@ -1,20 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 11.00 -# Visual Studio 2010 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestSharp.WindowsPhone.Mango", "RestSharp.WindowsPhone.Mango.csproj", "{71647E33-714F-4975-8368-71B075045272}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {71647E33-714F-4975-8368-71B075045272}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {71647E33-714F-4975-8368-71B075045272}.Debug|Any CPU.Build.0 = Debug|Any CPU - {71647E33-714F-4975-8368-71B075045272}.Release|Any CPU.ActiveCfg = Release|Any CPU - {71647E33-714F-4975-8368-71B075045272}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection -EndGlobal diff --git a/RestSharp.WindowsPhone.Mango/packages.config b/RestSharp.WindowsPhone.Mango/packages.config deleted file mode 100644 index 6b8deb9c9..000000000 --- a/RestSharp.WindowsPhone.Mango/packages.config +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/RestSharp.WindowsPhone/Properties/AssemblyInfo.cs b/RestSharp.WindowsPhone/Properties/AssemblyInfo.cs deleted file mode 100644 index c7f680a19..000000000 --- a/RestSharp.WindowsPhone/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("RestSharp.WindowsPhone")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("d6e5e03c-bbac-4d6a-8170-051b8225147c")] \ No newline at end of file diff --git a/RestSharp.WindowsPhone/RestSharp.WindowsPhone.csproj b/RestSharp.WindowsPhone/RestSharp.WindowsPhone.csproj deleted file mode 100644 index cd3f4f821..000000000 --- a/RestSharp.WindowsPhone/RestSharp.WindowsPhone.csproj +++ /dev/null @@ -1,301 +0,0 @@ - - - - Debug - AnyCPU - 10.0.20506 - 2.0 - {F4D48DF6-316E-4963-B5C1-59CA39B431B7} - {C089C8C0-30E0-4E22-80C0-CE093F111A43};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} - Library - Properties - RestSharp.WindowsPhone - RestSharp.WindowsPhone - v4.0 - $(TargetFrameworkVersion) - WindowsPhone71 - Silverlight - false - true - true - ..\ - true - - - true - full - false - Bin\Debug - TRACE;DEBUG;WINDOWS_PHONE - true - true - prompt - 4 - Bin\Debug\RestSharp.WindowsPhone.xml - - - pdbonly - true - Bin\Release - TRACE;WINDOWS_PHONE - true - true - prompt - 4 - Bin\Release\RestSharp.WindowsPhone.xml - 1591,1573,1658,1584,1574,1572 - true - - - - - - - - - - - - - - Authenticators\HttpBasicAuthenticator.cs - - - Authenticators\IAuthenticator.cs - - - Authenticators\NtlmAuthenticator.cs - - - Authenticators\OAuth1Authenticator.cs - - - Authenticators\OAuth2Authenticator.cs - - - Authenticators\OAuth\Extensions\CollectionExtensions.cs - - - Authenticators\OAuth\Extensions\OAuthExtensions.cs - - - Authenticators\OAuth\Extensions\StringExtensions.cs - - - Authenticators\OAuth\Extensions\TimeExtensions.cs - - - Authenticators\OAuth\HttpPostParameter.cs - - - Authenticators\OAuth\HttpPostParameterType.cs - - - Authenticators\OAuth\OAuthParameterHandling.cs - - - Authenticators\OAuth\OAuthSignatureMethod.cs - - - Authenticators\OAuth\OAuthSignatureTreatment.cs - - - Authenticators\OAuth\OAuthTools.cs - - - Authenticators\OAuth\OAuthType.cs - - - Authenticators\OAuth\OAuthWebQueryInfo.cs - - - Authenticators\OAuth\OAuthWorkflow.cs - - - Authenticators\OAuth\WebPair.cs - - - Authenticators\OAuth\WebPairCollection.cs - - - Authenticators\OAuth\WebParameter.cs - - - Authenticators\OAuth\WebParameterCollection.cs - - - Authenticators\SimpleAuthenticator.cs - - - Compression\ZLib\Crc32.cs - - - Compression\ZLib\FlushType.cs - - - Compression\ZLib\GZipStream.cs - - - Compression\ZLib\Inflate.cs - - - Compression\ZLib\InfTree.cs - - - Compression\ZLib\ZLib.cs - - - Compression\ZLib\ZLibCodec.cs - - - Compression\ZLib\ZLibConstants.cs - - - Compression\ZLib\ZLibStream.cs - - - Deserializers\DeserializeAsAttribute.cs - - - Deserializers\DotNetXmlDeserializer.cs - - - Deserializers\IDeserializer.cs - - - Deserializers\JsonDeserializer.cs - - - Deserializers\XmlAttributeDeserializer.cs - - - Deserializers\XmlDeserializer.cs - - - Enum.cs - - - Extensions\MiscExtensions.cs - - - Extensions\ReflectionExtensions.cs - - - Extensions\ResponseExtensions.cs - - - Extensions\StringExtensions.cs - - - Extensions\XmlExtensions.cs - - - FileParameter.cs - - - Http.Async.cs - - - Http.cs - - - HttpCookie.cs - - - HttpFile.cs - - - HttpHeader.cs - - - HttpParameter.cs - - - HttpResponse.cs - - - IHttp.cs - - - IHttpFactory.cs - - - IHttpResponse.cs - - - IRestClient.cs - - - IRestRequest.cs - - - IRestResponse.cs - - - Parameter.cs - - - RestClient.Async.cs - - - RestClient.cs - - - RestClientExtensions.cs - - - RestRequest.cs - - - RestResponse.cs - - - Serializers\DotNetXmlSerializer.cs - - - Serializers\ISerializer.cs - - - Serializers\JsonSerializer.cs - - - Serializers\SerializeAsAttribute.cs - - - Serializers\XmlSerializer.cs - - - SharedAssemblyInfo.cs - - - SimpleJson.cs - - - Validation\Require.cs - - - Validation\Validate.cs - - - RestResponseCookie.cs - - - RestRequestAsyncHandle.cs - - - - - - - - - - - - \ No newline at end of file diff --git a/RestSharp.WindowsPhone/packages.config b/RestSharp.WindowsPhone/packages.config deleted file mode 100644 index 6b8deb9c9..000000000 --- a/RestSharp.WindowsPhone/packages.config +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/RestSharp.WindowsPhone8/Properties/AssemblyInfo.cs b/RestSharp.WindowsPhone8/Properties/AssemblyInfo.cs deleted file mode 100644 index 0319a3712..000000000 --- a/RestSharp.WindowsPhone8/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("RestSharp.WindowsPhone")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("edd38716-2ef8-46c4-8730-c4ed8bc12eed")] \ No newline at end of file diff --git a/RestSharp.WindowsPhone8/RestSharp.WindowsPhone8.csproj b/RestSharp.WindowsPhone8/RestSharp.WindowsPhone8.csproj deleted file mode 100644 index 97e4edc57..000000000 --- a/RestSharp.WindowsPhone8/RestSharp.WindowsPhone8.csproj +++ /dev/null @@ -1,348 +0,0 @@ - - - - Debug - AnyCPU - 10.0.20506 - 2.0 - {71647E33-714F-4975-8368-71B075045272} - {C089C8C0-30E0-4E22-80C0-CE093F111A43};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} - Library - Properties - RestSharp.WindowsPhone - RestSharp.WindowsPhone - v8.0 - - - - - WindowsPhone - false - true - true - 11.0 - - - true - full - false - Bin\Debug - TRACE;DEBUG;WINDOWS_PHONE;MANGO;WP8 - true - true - prompt - 4 - - - false - - - pdbonly - true - Bin\Release - TRACE;WINDOWS_PHONE;MANGO;WP8 - true - true - prompt - 4 - - - false - - - true - Bin\x86\Debug - TRACE;DEBUG;WINDOWS_PHONE;MANGO - Bin\Debug\RestSharp.WindowsPhone.xml - true - full - - - prompt - MinimumRecommendedRules.ruleset - false - - - Bin\x86\Release - TRACE;WINDOWS_PHONE - Bin\Release\RestSharp.WindowsPhone.xml - true - true - pdbonly - - - prompt - MinimumRecommendedRules.ruleset - - - true - Bin\ARM\Debug - TRACE;DEBUG;WINDOWS_PHONE;MANGO - Bin\Debug\RestSharp.WindowsPhone.xml - true - full - - - prompt - MinimumRecommendedRules.ruleset - false - - - Bin\ARM\Release - TRACE;WINDOWS_PHONE - Bin\Release\RestSharp.WindowsPhone.xml - true - true - pdbonly - - - prompt - MinimumRecommendedRules.ruleset - - - - Extensions\ResponseStatusExtensions.cs - - - Authenticators\HttpBasicAuthenticator.cs - - - Authenticators\IAuthenticator.cs - - - Authenticators\NtlmAuthenticator.cs - - - Authenticators\OAuth1Authenticator.cs - - - Authenticators\OAuth2Authenticator.cs - - - Authenticators\OAuth\Extensions\CollectionExtensions.cs - - - Authenticators\OAuth\Extensions\OAuthExtensions.cs - - - Authenticators\OAuth\Extensions\StringExtensions.cs - - - Authenticators\OAuth\Extensions\TimeExtensions.cs - - - Authenticators\OAuth\HttpPostParameter.cs - - - Authenticators\OAuth\HttpPostParameterType.cs - - - Authenticators\OAuth\OAuthParameterHandling.cs - - - Authenticators\OAuth\OAuthSignatureMethod.cs - - - Authenticators\OAuth\OAuthSignatureTreatment.cs - - - Authenticators\OAuth\OAuthTools.cs - - - Authenticators\OAuth\OAuthType.cs - - - Authenticators\OAuth\OAuthWebQueryInfo.cs - - - Authenticators\OAuth\OAuthWorkflow.cs - - - Authenticators\OAuth\WebPair.cs - - - Authenticators\OAuth\WebPairCollection.cs - - - Authenticators\OAuth\WebParameter.cs - - - Authenticators\OAuth\WebParameterCollection.cs - - - Authenticators\SimpleAuthenticator.cs - - - Compression\ZLib\Crc32.cs - - - Compression\ZLib\FlushType.cs - - - Compression\ZLib\GZipStream.cs - - - Compression\ZLib\Inflate.cs - - - Compression\ZLib\InfTree.cs - - - Compression\ZLib\ZLib.cs - - - Compression\ZLib\ZLibCodec.cs - - - Compression\ZLib\ZLibConstants.cs - - - Compression\ZLib\ZLibStream.cs - - - Deserializers\DeserializeAsAttribute.cs - - - Deserializers\DotNetXmlDeserializer.cs - - - Deserializers\IDeserializer.cs - - - Deserializers\JsonDeserializer.cs - - - Deserializers\XmlAttributeDeserializer.cs - - - Deserializers\XmlDeserializer.cs - - - Enum.cs - - - Extensions\MiscExtensions.cs - - - Extensions\ReflectionExtensions.cs - - - Extensions\StringExtensions.cs - - - Extensions\ResponseExtensions.cs - - - Extensions\XmlExtensions.cs - - - FileParameter.cs - - - Http.Async.cs - - - Http.cs - - - HttpCookie.cs - - - HttpFile.cs - - - HttpHeader.cs - - - HttpParameter.cs - - - HttpResponse.cs - - - IHttp.cs - - - IHttpFactory.cs - - - IHttpResponse.cs - - - IRestClient.cs - - - IRestRequest.cs - - - IRestResponse.cs - - - Parameter.cs - - - RestClient.Async.cs - - - RestClient.cs - - - RestClientExtensions.cs - - - RestRequest.cs - - - RestResponse.cs - - - RestResponseCookie.cs - - - Serializers\DotNetXmlSerializer.cs - - - Serializers\ISerializer.cs - - - Serializers\JsonSerializer.cs - - - Serializers\SerializeAsAttribute.cs - - - Serializers\XmlSerializer.cs - - - SharedAssemblyInfo.cs - - - SimpleJson.cs - - - Validation\Require.cs - - - Validation\Validate.cs - - - RestRequestAsyncHandle.cs - - - - - - Designer - - - - - - - \ No newline at end of file diff --git a/RestSharp.WindowsPhone8/packages.config b/RestSharp.WindowsPhone8/packages.config deleted file mode 100644 index 6b8deb9c9..000000000 --- a/RestSharp.WindowsPhone8/packages.config +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/RestSharp.sln b/RestSharp.sln index ffe12b3a3..0f86b90e0 100644 --- a/RestSharp.sln +++ b/RestSharp.sln @@ -1,123 +1,535 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 2012 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestSharp", "RestSharp\RestSharp.csproj", "{2ECECFBF-5F3E-40EE-A963-72336DC7ABE2}" +# Visual Studio Version 17 +VisualStudioVersion = 17.3.32811.315 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RestSharp", "src\RestSharp\RestSharp.csproj", "{43A1D5D2-650D-40DD-A6C0-14F92689C70B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestSharp.Tests", "RestSharp.Tests\RestSharp.Tests.csproj", "{1464E4AC-18BB-4F23-8A0B-68196F9E1871}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{9051DDA0-E563-45D5-9504-085EBAACF469}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestSharp.IntegrationTests", "RestSharp.IntegrationTests\RestSharp.IntegrationTests.csproj", "{47D3EBB9-0300-4AF8-BAC5-740D51454A63}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RestSharp.Tests", "test\RestSharp.Tests\RestSharp.Tests.csproj", "{B1C55C9B-3287-4EB2-8ADD-795DBC77013D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestSharp.Silverlight", "RestSharp.Silverlight\RestSharp.Silverlight.csproj", "{11F84600-0978-48B9-A28F-63B3781E54B3}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RestSharp.Tests.Integrated", "test\RestSharp.Tests.Integrated\RestSharp.Tests.Integrated.csproj", "{AC3B3DDC-F011-4E19-8C9B-F748B19ED3C0}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestSharp.WindowsPhone", "RestSharp.WindowsPhone\RestSharp.WindowsPhone.csproj", "{F4D48DF6-316E-4963-B5C1-59CA39B431B7}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RestSharp.Serializers.NewtonsoftJson", "src\RestSharp.Serializers.NewtonsoftJson\RestSharp.Serializers.NewtonsoftJson.csproj", "{4205A187-9732-4DA8-B0BE-77A2C6B8C6A1}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "NuGet", "NuGet", "{E709A928-A45C-4622-A35C-CCD8EE44CA80}" - ProjectSection(SolutionItems) = preProject - package.cmd = package.cmd - readme.txt = readme.txt - restsharp.nuspec = restsharp.nuspec - EndProjectSection +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Serializers", "Serializers", "{8C7B43EB-2F93-483C-B433-E28F9386AD67}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestSharp.Net4", "RestSharp.Net4\RestSharp.Net4.csproj", "{5FF943A5-260F-4042-B4CE-C4977BAD4EBB}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RestSharp.Tests.Shared", "test\RestSharp.Tests.Shared\RestSharp.Tests.Shared.csproj", "{73896669-F05C-41AC-9F6F-A11F549EDEDC}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{1B3F12F6-D32B-48C4-98D7-AB448EB78811}" - ProjectSection(SolutionItems) = preProject - .nuget\NuGet.exe = .nuget\NuGet.exe - .nuget\NuGet.targets = .nuget\NuGet.targets - EndProjectSection +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RestSharp.Tests.Serializers.Json", "test\RestSharp.Tests.Serializers.Json\RestSharp.Tests.Serializers.Json.csproj", "{8BF81225-2F85-4412-AD18-6579CBA1879B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestSharp.Build", "RestSharp.Build\RestSharp.Build.csproj", "{CCC30138-3D68-44D8-AF1A-D22F769EE8DC}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Perf", "Perf", "{1C42C435-8826-4044-8775-A1DA40EF4866}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Meta", "Meta", "{C5B02FAA-6A0A-4BF9-BBD4-82FD2DCBF669}" - ProjectSection(SolutionItems) = preProject - CONTRIBUTING.markdown = CONTRIBUTING.markdown - README.markdown = README.markdown - readme.txt = readme.txt - releasenotes.markdown = releasenotes.markdown - EndProjectSection +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RestSharp.Benchmarks", "benchmarks\RestSharp.Benchmarks\RestSharp.Benchmarks.csproj", "{997AEFE5-D7D4-4033-A31A-07F476D6FE5D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RestSharp.InteractiveTests", "test\RestSharp.InteractiveTests\RestSharp.InteractiveTests.csproj", "{6D7D1D60-4473-4C52-800C-9B892C6640A5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RestSharp.Tests.Serializers.Xml", "test\RestSharp.Tests.Serializers.Xml\RestSharp.Tests.Serializers.Xml.csproj", "{E6D94C12-9AD7-46E6-AB62-3676F25FDE51}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RestSharp.Serializers.Xml", "src\RestSharp.Serializers.Xml\RestSharp.Serializers.Xml.csproj", "{4A35B1C5-520D-4267-BA70-2DCEAC0A5662}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RestSharp.Serializers.CsvHelper", "src\RestSharp.Serializers.CsvHelper\RestSharp.Serializers.CsvHelper.csproj", "{2150E333-8FDC-42A3-9474-1A3956D46DE8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RestSharp.Tests.Serializers.Csv", "test\RestSharp.Tests.Serializers.Csv\RestSharp.Tests.Serializers.Csv.csproj", "{E6D94FFD-7811-40BE-ABC4-6D6AB41F0060}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SourceGen", "SourceGen", "{55B8F371-B2BA-4DEE-AB98-5BAB8A21B1C2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SourceGenerator", "gen\SourceGenerator\SourceGenerator.csproj", "{FE778406-ADCF-45A1-B775-A054B55BFC50}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestSharp.Extensions.DependencyInjection", "src\RestSharp.Extensions.DependencyInjection\RestSharp.Extensions.DependencyInjection.csproj", "{92A6F3CA-100F-4D9D-9742-B62267D445B6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestSharp.Tests.DependencyInjection", "test\RestSharp.Tests.DependencyInjection\RestSharp.Tests.DependencyInjection.csproj", "{602FF788-E926-4404-B3A2-D6B778A5FFB7}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug.Appveyor|Any CPU = Debug.Appveyor|Any CPU + Debug.Appveyor|ARM = Debug.Appveyor|ARM + Debug.Appveyor|Mixed Platforms = Debug.Appveyor|Mixed Platforms + Debug.Appveyor|x64 = Debug.Appveyor|x64 + Debug.Appveyor|x86 = Debug.Appveyor|x86 Debug|Any CPU = Debug|Any CPU + Debug|ARM = Debug|ARM Debug|Mixed Platforms = Debug|Mixed Platforms + Debug|x64 = Debug|x64 Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|ARM = Release|ARM Release|Mixed Platforms = Release|Mixed Platforms + Release|x64 = Release|x64 Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {2ECECFBF-5F3E-40EE-A963-72336DC7ABE2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2ECECFBF-5F3E-40EE-A963-72336DC7ABE2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2ECECFBF-5F3E-40EE-A963-72336DC7ABE2}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {2ECECFBF-5F3E-40EE-A963-72336DC7ABE2}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {2ECECFBF-5F3E-40EE-A963-72336DC7ABE2}.Debug|x86.ActiveCfg = Debug|Any CPU - {2ECECFBF-5F3E-40EE-A963-72336DC7ABE2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2ECECFBF-5F3E-40EE-A963-72336DC7ABE2}.Release|Any CPU.Build.0 = Release|Any CPU - {2ECECFBF-5F3E-40EE-A963-72336DC7ABE2}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {2ECECFBF-5F3E-40EE-A963-72336DC7ABE2}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {2ECECFBF-5F3E-40EE-A963-72336DC7ABE2}.Release|x86.ActiveCfg = Release|Any CPU - {1464E4AC-18BB-4F23-8A0B-68196F9E1871}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1464E4AC-18BB-4F23-8A0B-68196F9E1871}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1464E4AC-18BB-4F23-8A0B-68196F9E1871}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {1464E4AC-18BB-4F23-8A0B-68196F9E1871}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {1464E4AC-18BB-4F23-8A0B-68196F9E1871}.Debug|x86.ActiveCfg = Debug|Any CPU - {1464E4AC-18BB-4F23-8A0B-68196F9E1871}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1464E4AC-18BB-4F23-8A0B-68196F9E1871}.Release|Any CPU.Build.0 = Release|Any CPU - {1464E4AC-18BB-4F23-8A0B-68196F9E1871}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {1464E4AC-18BB-4F23-8A0B-68196F9E1871}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {1464E4AC-18BB-4F23-8A0B-68196F9E1871}.Release|x86.ActiveCfg = Release|Any CPU - {47D3EBB9-0300-4AF8-BAC5-740D51454A63}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {47D3EBB9-0300-4AF8-BAC5-740D51454A63}.Debug|Any CPU.Build.0 = Debug|Any CPU - {47D3EBB9-0300-4AF8-BAC5-740D51454A63}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {47D3EBB9-0300-4AF8-BAC5-740D51454A63}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {47D3EBB9-0300-4AF8-BAC5-740D51454A63}.Debug|x86.ActiveCfg = Debug|Any CPU - {47D3EBB9-0300-4AF8-BAC5-740D51454A63}.Release|Any CPU.ActiveCfg = Release|Any CPU - {47D3EBB9-0300-4AF8-BAC5-740D51454A63}.Release|Any CPU.Build.0 = Release|Any CPU - {47D3EBB9-0300-4AF8-BAC5-740D51454A63}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {47D3EBB9-0300-4AF8-BAC5-740D51454A63}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {47D3EBB9-0300-4AF8-BAC5-740D51454A63}.Release|x86.ActiveCfg = Release|Any CPU - {11F84600-0978-48B9-A28F-63B3781E54B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {11F84600-0978-48B9-A28F-63B3781E54B3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {11F84600-0978-48B9-A28F-63B3781E54B3}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {11F84600-0978-48B9-A28F-63B3781E54B3}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {11F84600-0978-48B9-A28F-63B3781E54B3}.Debug|x86.ActiveCfg = Debug|Any CPU - {11F84600-0978-48B9-A28F-63B3781E54B3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {11F84600-0978-48B9-A28F-63B3781E54B3}.Release|Any CPU.Build.0 = Release|Any CPU - {11F84600-0978-48B9-A28F-63B3781E54B3}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {11F84600-0978-48B9-A28F-63B3781E54B3}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {11F84600-0978-48B9-A28F-63B3781E54B3}.Release|x86.ActiveCfg = Release|Any CPU - {F4D48DF6-316E-4963-B5C1-59CA39B431B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F4D48DF6-316E-4963-B5C1-59CA39B431B7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F4D48DF6-316E-4963-B5C1-59CA39B431B7}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {F4D48DF6-316E-4963-B5C1-59CA39B431B7}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {F4D48DF6-316E-4963-B5C1-59CA39B431B7}.Debug|x86.ActiveCfg = Debug|Any CPU - {F4D48DF6-316E-4963-B5C1-59CA39B431B7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F4D48DF6-316E-4963-B5C1-59CA39B431B7}.Release|Any CPU.Build.0 = Release|Any CPU - {F4D48DF6-316E-4963-B5C1-59CA39B431B7}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {F4D48DF6-316E-4963-B5C1-59CA39B431B7}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {F4D48DF6-316E-4963-B5C1-59CA39B431B7}.Release|x86.ActiveCfg = Release|Any CPU - {5FF943A5-260F-4042-B4CE-C4977BAD4EBB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5FF943A5-260F-4042-B4CE-C4977BAD4EBB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5FF943A5-260F-4042-B4CE-C4977BAD4EBB}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {5FF943A5-260F-4042-B4CE-C4977BAD4EBB}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {5FF943A5-260F-4042-B4CE-C4977BAD4EBB}.Debug|x86.ActiveCfg = Debug|Any CPU - {5FF943A5-260F-4042-B4CE-C4977BAD4EBB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5FF943A5-260F-4042-B4CE-C4977BAD4EBB}.Release|Any CPU.Build.0 = Release|Any CPU - {5FF943A5-260F-4042-B4CE-C4977BAD4EBB}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {5FF943A5-260F-4042-B4CE-C4977BAD4EBB}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {5FF943A5-260F-4042-B4CE-C4977BAD4EBB}.Release|x86.ActiveCfg = Release|Any CPU - {CCC30138-3D68-44D8-AF1A-D22F769EE8DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CCC30138-3D68-44D8-AF1A-D22F769EE8DC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CCC30138-3D68-44D8-AF1A-D22F769EE8DC}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {CCC30138-3D68-44D8-AF1A-D22F769EE8DC}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {CCC30138-3D68-44D8-AF1A-D22F769EE8DC}.Debug|x86.ActiveCfg = Debug|Any CPU - {CCC30138-3D68-44D8-AF1A-D22F769EE8DC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CCC30138-3D68-44D8-AF1A-D22F769EE8DC}.Release|Any CPU.Build.0 = Release|Any CPU - {CCC30138-3D68-44D8-AF1A-D22F769EE8DC}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {CCC30138-3D68-44D8-AF1A-D22F769EE8DC}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {CCC30138-3D68-44D8-AF1A-D22F769EE8DC}.Release|x86.ActiveCfg = Release|Any CPU + {43A1D5D2-650D-40DD-A6C0-14F92689C70B}.Debug.Appveyor|Any CPU.ActiveCfg = Debug|Any CPU + {43A1D5D2-650D-40DD-A6C0-14F92689C70B}.Debug.Appveyor|Any CPU.Build.0 = Debug|Any CPU + {43A1D5D2-650D-40DD-A6C0-14F92689C70B}.Debug.Appveyor|ARM.ActiveCfg = Debug|Any CPU + {43A1D5D2-650D-40DD-A6C0-14F92689C70B}.Debug.Appveyor|ARM.Build.0 = Debug|Any CPU + {43A1D5D2-650D-40DD-A6C0-14F92689C70B}.Debug.Appveyor|Mixed Platforms.ActiveCfg = Debug|Any CPU + {43A1D5D2-650D-40DD-A6C0-14F92689C70B}.Debug.Appveyor|Mixed Platforms.Build.0 = Debug|Any CPU + {43A1D5D2-650D-40DD-A6C0-14F92689C70B}.Debug.Appveyor|x64.ActiveCfg = Debug|Any CPU + {43A1D5D2-650D-40DD-A6C0-14F92689C70B}.Debug.Appveyor|x64.Build.0 = Debug|Any CPU + {43A1D5D2-650D-40DD-A6C0-14F92689C70B}.Debug.Appveyor|x86.ActiveCfg = Debug|Any CPU + {43A1D5D2-650D-40DD-A6C0-14F92689C70B}.Debug.Appveyor|x86.Build.0 = Debug|Any CPU + {43A1D5D2-650D-40DD-A6C0-14F92689C70B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {43A1D5D2-650D-40DD-A6C0-14F92689C70B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {43A1D5D2-650D-40DD-A6C0-14F92689C70B}.Debug|ARM.ActiveCfg = Debug|Any CPU + {43A1D5D2-650D-40DD-A6C0-14F92689C70B}.Debug|ARM.Build.0 = Debug|Any CPU + {43A1D5D2-650D-40DD-A6C0-14F92689C70B}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {43A1D5D2-650D-40DD-A6C0-14F92689C70B}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {43A1D5D2-650D-40DD-A6C0-14F92689C70B}.Debug|x64.ActiveCfg = Debug|Any CPU + {43A1D5D2-650D-40DD-A6C0-14F92689C70B}.Debug|x64.Build.0 = Debug|Any CPU + {43A1D5D2-650D-40DD-A6C0-14F92689C70B}.Debug|x86.ActiveCfg = Debug|Any CPU + {43A1D5D2-650D-40DD-A6C0-14F92689C70B}.Debug|x86.Build.0 = Debug|Any CPU + {43A1D5D2-650D-40DD-A6C0-14F92689C70B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {43A1D5D2-650D-40DD-A6C0-14F92689C70B}.Release|Any CPU.Build.0 = Release|Any CPU + {43A1D5D2-650D-40DD-A6C0-14F92689C70B}.Release|ARM.ActiveCfg = Release|Any CPU + {43A1D5D2-650D-40DD-A6C0-14F92689C70B}.Release|ARM.Build.0 = Release|Any CPU + {43A1D5D2-650D-40DD-A6C0-14F92689C70B}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {43A1D5D2-650D-40DD-A6C0-14F92689C70B}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {43A1D5D2-650D-40DD-A6C0-14F92689C70B}.Release|x64.ActiveCfg = Release|Any CPU + {43A1D5D2-650D-40DD-A6C0-14F92689C70B}.Release|x64.Build.0 = Release|Any CPU + {43A1D5D2-650D-40DD-A6C0-14F92689C70B}.Release|x86.ActiveCfg = Release|Any CPU + {43A1D5D2-650D-40DD-A6C0-14F92689C70B}.Release|x86.Build.0 = Release|Any CPU + {B1C55C9B-3287-4EB2-8ADD-795DBC77013D}.Debug.Appveyor|Any CPU.ActiveCfg = Debug|Any CPU + {B1C55C9B-3287-4EB2-8ADD-795DBC77013D}.Debug.Appveyor|Any CPU.Build.0 = Debug|Any CPU + {B1C55C9B-3287-4EB2-8ADD-795DBC77013D}.Debug.Appveyor|ARM.ActiveCfg = Debug|Any CPU + {B1C55C9B-3287-4EB2-8ADD-795DBC77013D}.Debug.Appveyor|ARM.Build.0 = Debug|Any CPU + {B1C55C9B-3287-4EB2-8ADD-795DBC77013D}.Debug.Appveyor|Mixed Platforms.ActiveCfg = Debug|Any CPU + {B1C55C9B-3287-4EB2-8ADD-795DBC77013D}.Debug.Appveyor|Mixed Platforms.Build.0 = Debug|Any CPU + {B1C55C9B-3287-4EB2-8ADD-795DBC77013D}.Debug.Appveyor|x64.ActiveCfg = Debug|Any CPU + {B1C55C9B-3287-4EB2-8ADD-795DBC77013D}.Debug.Appveyor|x64.Build.0 = Debug|Any CPU + {B1C55C9B-3287-4EB2-8ADD-795DBC77013D}.Debug.Appveyor|x86.ActiveCfg = Debug|Any CPU + {B1C55C9B-3287-4EB2-8ADD-795DBC77013D}.Debug.Appveyor|x86.Build.0 = Debug|Any CPU + {B1C55C9B-3287-4EB2-8ADD-795DBC77013D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B1C55C9B-3287-4EB2-8ADD-795DBC77013D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B1C55C9B-3287-4EB2-8ADD-795DBC77013D}.Debug|ARM.ActiveCfg = Debug|Any CPU + {B1C55C9B-3287-4EB2-8ADD-795DBC77013D}.Debug|ARM.Build.0 = Debug|Any CPU + {B1C55C9B-3287-4EB2-8ADD-795DBC77013D}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {B1C55C9B-3287-4EB2-8ADD-795DBC77013D}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {B1C55C9B-3287-4EB2-8ADD-795DBC77013D}.Debug|x64.ActiveCfg = Debug|Any CPU + {B1C55C9B-3287-4EB2-8ADD-795DBC77013D}.Debug|x64.Build.0 = Debug|Any CPU + {B1C55C9B-3287-4EB2-8ADD-795DBC77013D}.Debug|x86.ActiveCfg = Debug|Any CPU + {B1C55C9B-3287-4EB2-8ADD-795DBC77013D}.Debug|x86.Build.0 = Debug|Any CPU + {B1C55C9B-3287-4EB2-8ADD-795DBC77013D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B1C55C9B-3287-4EB2-8ADD-795DBC77013D}.Release|Any CPU.Build.0 = Release|Any CPU + {B1C55C9B-3287-4EB2-8ADD-795DBC77013D}.Release|ARM.ActiveCfg = Release|Any CPU + {B1C55C9B-3287-4EB2-8ADD-795DBC77013D}.Release|ARM.Build.0 = Release|Any CPU + {B1C55C9B-3287-4EB2-8ADD-795DBC77013D}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {B1C55C9B-3287-4EB2-8ADD-795DBC77013D}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {B1C55C9B-3287-4EB2-8ADD-795DBC77013D}.Release|x64.ActiveCfg = Release|Any CPU + {B1C55C9B-3287-4EB2-8ADD-795DBC77013D}.Release|x64.Build.0 = Release|Any CPU + {B1C55C9B-3287-4EB2-8ADD-795DBC77013D}.Release|x86.ActiveCfg = Release|Any CPU + {B1C55C9B-3287-4EB2-8ADD-795DBC77013D}.Release|x86.Build.0 = Release|Any CPU + {AC3B3DDC-F011-4E19-8C9B-F748B19ED3C0}.Debug.Appveyor|Any CPU.ActiveCfg = Debug|Any CPU + {AC3B3DDC-F011-4E19-8C9B-F748B19ED3C0}.Debug.Appveyor|Any CPU.Build.0 = Debug|Any CPU + {AC3B3DDC-F011-4E19-8C9B-F748B19ED3C0}.Debug.Appveyor|ARM.ActiveCfg = Debug|Any CPU + {AC3B3DDC-F011-4E19-8C9B-F748B19ED3C0}.Debug.Appveyor|ARM.Build.0 = Debug|Any CPU + {AC3B3DDC-F011-4E19-8C9B-F748B19ED3C0}.Debug.Appveyor|Mixed Platforms.ActiveCfg = Debug|Any CPU + {AC3B3DDC-F011-4E19-8C9B-F748B19ED3C0}.Debug.Appveyor|Mixed Platforms.Build.0 = Debug|Any CPU + {AC3B3DDC-F011-4E19-8C9B-F748B19ED3C0}.Debug.Appveyor|x64.ActiveCfg = Debug|Any CPU + {AC3B3DDC-F011-4E19-8C9B-F748B19ED3C0}.Debug.Appveyor|x64.Build.0 = Debug|Any CPU + {AC3B3DDC-F011-4E19-8C9B-F748B19ED3C0}.Debug.Appveyor|x86.ActiveCfg = Debug|Any CPU + {AC3B3DDC-F011-4E19-8C9B-F748B19ED3C0}.Debug.Appveyor|x86.Build.0 = Debug|Any CPU + {AC3B3DDC-F011-4E19-8C9B-F748B19ED3C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AC3B3DDC-F011-4E19-8C9B-F748B19ED3C0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AC3B3DDC-F011-4E19-8C9B-F748B19ED3C0}.Debug|ARM.ActiveCfg = Debug|Any CPU + {AC3B3DDC-F011-4E19-8C9B-F748B19ED3C0}.Debug|ARM.Build.0 = Debug|Any CPU + {AC3B3DDC-F011-4E19-8C9B-F748B19ED3C0}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {AC3B3DDC-F011-4E19-8C9B-F748B19ED3C0}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {AC3B3DDC-F011-4E19-8C9B-F748B19ED3C0}.Debug|x64.ActiveCfg = Debug|Any CPU + {AC3B3DDC-F011-4E19-8C9B-F748B19ED3C0}.Debug|x64.Build.0 = Debug|Any CPU + {AC3B3DDC-F011-4E19-8C9B-F748B19ED3C0}.Debug|x86.ActiveCfg = Debug|Any CPU + {AC3B3DDC-F011-4E19-8C9B-F748B19ED3C0}.Debug|x86.Build.0 = Debug|Any CPU + {AC3B3DDC-F011-4E19-8C9B-F748B19ED3C0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AC3B3DDC-F011-4E19-8C9B-F748B19ED3C0}.Release|Any CPU.Build.0 = Release|Any CPU + {AC3B3DDC-F011-4E19-8C9B-F748B19ED3C0}.Release|ARM.ActiveCfg = Release|Any CPU + {AC3B3DDC-F011-4E19-8C9B-F748B19ED3C0}.Release|ARM.Build.0 = Release|Any CPU + {AC3B3DDC-F011-4E19-8C9B-F748B19ED3C0}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {AC3B3DDC-F011-4E19-8C9B-F748B19ED3C0}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {AC3B3DDC-F011-4E19-8C9B-F748B19ED3C0}.Release|x64.ActiveCfg = Release|Any CPU + {AC3B3DDC-F011-4E19-8C9B-F748B19ED3C0}.Release|x64.Build.0 = Release|Any CPU + {AC3B3DDC-F011-4E19-8C9B-F748B19ED3C0}.Release|x86.ActiveCfg = Release|Any CPU + {AC3B3DDC-F011-4E19-8C9B-F748B19ED3C0}.Release|x86.Build.0 = Release|Any CPU + {4205A187-9732-4DA8-B0BE-77A2C6B8C6A1}.Debug.Appveyor|Any CPU.ActiveCfg = Debug|Any CPU + {4205A187-9732-4DA8-B0BE-77A2C6B8C6A1}.Debug.Appveyor|Any CPU.Build.0 = Debug|Any CPU + {4205A187-9732-4DA8-B0BE-77A2C6B8C6A1}.Debug.Appveyor|ARM.ActiveCfg = Debug|Any CPU + {4205A187-9732-4DA8-B0BE-77A2C6B8C6A1}.Debug.Appveyor|ARM.Build.0 = Debug|Any CPU + {4205A187-9732-4DA8-B0BE-77A2C6B8C6A1}.Debug.Appveyor|Mixed Platforms.ActiveCfg = Debug|Any CPU + {4205A187-9732-4DA8-B0BE-77A2C6B8C6A1}.Debug.Appveyor|Mixed Platforms.Build.0 = Debug|Any CPU + {4205A187-9732-4DA8-B0BE-77A2C6B8C6A1}.Debug.Appveyor|x64.ActiveCfg = Debug|Any CPU + {4205A187-9732-4DA8-B0BE-77A2C6B8C6A1}.Debug.Appveyor|x64.Build.0 = Debug|Any CPU + {4205A187-9732-4DA8-B0BE-77A2C6B8C6A1}.Debug.Appveyor|x86.ActiveCfg = Debug|Any CPU + {4205A187-9732-4DA8-B0BE-77A2C6B8C6A1}.Debug.Appveyor|x86.Build.0 = Debug|Any CPU + {4205A187-9732-4DA8-B0BE-77A2C6B8C6A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4205A187-9732-4DA8-B0BE-77A2C6B8C6A1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4205A187-9732-4DA8-B0BE-77A2C6B8C6A1}.Debug|ARM.ActiveCfg = Debug|Any CPU + {4205A187-9732-4DA8-B0BE-77A2C6B8C6A1}.Debug|ARM.Build.0 = Debug|Any CPU + {4205A187-9732-4DA8-B0BE-77A2C6B8C6A1}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {4205A187-9732-4DA8-B0BE-77A2C6B8C6A1}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {4205A187-9732-4DA8-B0BE-77A2C6B8C6A1}.Debug|x64.ActiveCfg = Debug|Any CPU + {4205A187-9732-4DA8-B0BE-77A2C6B8C6A1}.Debug|x64.Build.0 = Debug|Any CPU + {4205A187-9732-4DA8-B0BE-77A2C6B8C6A1}.Debug|x86.ActiveCfg = Debug|Any CPU + {4205A187-9732-4DA8-B0BE-77A2C6B8C6A1}.Debug|x86.Build.0 = Debug|Any CPU + {4205A187-9732-4DA8-B0BE-77A2C6B8C6A1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4205A187-9732-4DA8-B0BE-77A2C6B8C6A1}.Release|Any CPU.Build.0 = Release|Any CPU + {4205A187-9732-4DA8-B0BE-77A2C6B8C6A1}.Release|ARM.ActiveCfg = Release|Any CPU + {4205A187-9732-4DA8-B0BE-77A2C6B8C6A1}.Release|ARM.Build.0 = Release|Any CPU + {4205A187-9732-4DA8-B0BE-77A2C6B8C6A1}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {4205A187-9732-4DA8-B0BE-77A2C6B8C6A1}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {4205A187-9732-4DA8-B0BE-77A2C6B8C6A1}.Release|x64.ActiveCfg = Release|Any CPU + {4205A187-9732-4DA8-B0BE-77A2C6B8C6A1}.Release|x64.Build.0 = Release|Any CPU + {4205A187-9732-4DA8-B0BE-77A2C6B8C6A1}.Release|x86.ActiveCfg = Release|Any CPU + {4205A187-9732-4DA8-B0BE-77A2C6B8C6A1}.Release|x86.Build.0 = Release|Any CPU + {73896669-F05C-41AC-9F6F-A11F549EDEDC}.Debug.Appveyor|Any CPU.ActiveCfg = Debug|Any CPU + {73896669-F05C-41AC-9F6F-A11F549EDEDC}.Debug.Appveyor|Any CPU.Build.0 = Debug|Any CPU + {73896669-F05C-41AC-9F6F-A11F549EDEDC}.Debug.Appveyor|ARM.ActiveCfg = Debug|Any CPU + {73896669-F05C-41AC-9F6F-A11F549EDEDC}.Debug.Appveyor|ARM.Build.0 = Debug|Any CPU + {73896669-F05C-41AC-9F6F-A11F549EDEDC}.Debug.Appveyor|Mixed Platforms.ActiveCfg = Debug|Any CPU + {73896669-F05C-41AC-9F6F-A11F549EDEDC}.Debug.Appveyor|Mixed Platforms.Build.0 = Debug|Any CPU + {73896669-F05C-41AC-9F6F-A11F549EDEDC}.Debug.Appveyor|x64.ActiveCfg = Debug|Any CPU + {73896669-F05C-41AC-9F6F-A11F549EDEDC}.Debug.Appveyor|x64.Build.0 = Debug|Any CPU + {73896669-F05C-41AC-9F6F-A11F549EDEDC}.Debug.Appveyor|x86.ActiveCfg = Debug|Any CPU + {73896669-F05C-41AC-9F6F-A11F549EDEDC}.Debug.Appveyor|x86.Build.0 = Debug|Any CPU + {73896669-F05C-41AC-9F6F-A11F549EDEDC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {73896669-F05C-41AC-9F6F-A11F549EDEDC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {73896669-F05C-41AC-9F6F-A11F549EDEDC}.Debug|ARM.ActiveCfg = Debug|Any CPU + {73896669-F05C-41AC-9F6F-A11F549EDEDC}.Debug|ARM.Build.0 = Debug|Any CPU + {73896669-F05C-41AC-9F6F-A11F549EDEDC}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {73896669-F05C-41AC-9F6F-A11F549EDEDC}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {73896669-F05C-41AC-9F6F-A11F549EDEDC}.Debug|x64.ActiveCfg = Debug|Any CPU + {73896669-F05C-41AC-9F6F-A11F549EDEDC}.Debug|x64.Build.0 = Debug|Any CPU + {73896669-F05C-41AC-9F6F-A11F549EDEDC}.Debug|x86.ActiveCfg = Debug|Any CPU + {73896669-F05C-41AC-9F6F-A11F549EDEDC}.Debug|x86.Build.0 = Debug|Any CPU + {73896669-F05C-41AC-9F6F-A11F549EDEDC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {73896669-F05C-41AC-9F6F-A11F549EDEDC}.Release|Any CPU.Build.0 = Release|Any CPU + {73896669-F05C-41AC-9F6F-A11F549EDEDC}.Release|ARM.ActiveCfg = Release|Any CPU + {73896669-F05C-41AC-9F6F-A11F549EDEDC}.Release|ARM.Build.0 = Release|Any CPU + {73896669-F05C-41AC-9F6F-A11F549EDEDC}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {73896669-F05C-41AC-9F6F-A11F549EDEDC}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {73896669-F05C-41AC-9F6F-A11F549EDEDC}.Release|x64.ActiveCfg = Release|Any CPU + {73896669-F05C-41AC-9F6F-A11F549EDEDC}.Release|x64.Build.0 = Release|Any CPU + {73896669-F05C-41AC-9F6F-A11F549EDEDC}.Release|x86.ActiveCfg = Release|Any CPU + {73896669-F05C-41AC-9F6F-A11F549EDEDC}.Release|x86.Build.0 = Release|Any CPU + {8BF81225-2F85-4412-AD18-6579CBA1879B}.Debug.Appveyor|Any CPU.ActiveCfg = Debug|Any CPU + {8BF81225-2F85-4412-AD18-6579CBA1879B}.Debug.Appveyor|Any CPU.Build.0 = Debug|Any CPU + {8BF81225-2F85-4412-AD18-6579CBA1879B}.Debug.Appveyor|ARM.ActiveCfg = Debug|Any CPU + {8BF81225-2F85-4412-AD18-6579CBA1879B}.Debug.Appveyor|ARM.Build.0 = Debug|Any CPU + {8BF81225-2F85-4412-AD18-6579CBA1879B}.Debug.Appveyor|Mixed Platforms.ActiveCfg = Debug|Any CPU + {8BF81225-2F85-4412-AD18-6579CBA1879B}.Debug.Appveyor|Mixed Platforms.Build.0 = Debug|Any CPU + {8BF81225-2F85-4412-AD18-6579CBA1879B}.Debug.Appveyor|x64.ActiveCfg = Debug|Any CPU + {8BF81225-2F85-4412-AD18-6579CBA1879B}.Debug.Appveyor|x64.Build.0 = Debug|Any CPU + {8BF81225-2F85-4412-AD18-6579CBA1879B}.Debug.Appveyor|x86.ActiveCfg = Debug|Any CPU + {8BF81225-2F85-4412-AD18-6579CBA1879B}.Debug.Appveyor|x86.Build.0 = Debug|Any CPU + {8BF81225-2F85-4412-AD18-6579CBA1879B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8BF81225-2F85-4412-AD18-6579CBA1879B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8BF81225-2F85-4412-AD18-6579CBA1879B}.Debug|ARM.ActiveCfg = Debug|Any CPU + {8BF81225-2F85-4412-AD18-6579CBA1879B}.Debug|ARM.Build.0 = Debug|Any CPU + {8BF81225-2F85-4412-AD18-6579CBA1879B}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {8BF81225-2F85-4412-AD18-6579CBA1879B}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {8BF81225-2F85-4412-AD18-6579CBA1879B}.Debug|x64.ActiveCfg = Debug|Any CPU + {8BF81225-2F85-4412-AD18-6579CBA1879B}.Debug|x64.Build.0 = Debug|Any CPU + {8BF81225-2F85-4412-AD18-6579CBA1879B}.Debug|x86.ActiveCfg = Debug|Any CPU + {8BF81225-2F85-4412-AD18-6579CBA1879B}.Debug|x86.Build.0 = Debug|Any CPU + {8BF81225-2F85-4412-AD18-6579CBA1879B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8BF81225-2F85-4412-AD18-6579CBA1879B}.Release|Any CPU.Build.0 = Release|Any CPU + {8BF81225-2F85-4412-AD18-6579CBA1879B}.Release|ARM.ActiveCfg = Release|Any CPU + {8BF81225-2F85-4412-AD18-6579CBA1879B}.Release|ARM.Build.0 = Release|Any CPU + {8BF81225-2F85-4412-AD18-6579CBA1879B}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {8BF81225-2F85-4412-AD18-6579CBA1879B}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {8BF81225-2F85-4412-AD18-6579CBA1879B}.Release|x64.ActiveCfg = Release|Any CPU + {8BF81225-2F85-4412-AD18-6579CBA1879B}.Release|x64.Build.0 = Release|Any CPU + {8BF81225-2F85-4412-AD18-6579CBA1879B}.Release|x86.ActiveCfg = Release|Any CPU + {8BF81225-2F85-4412-AD18-6579CBA1879B}.Release|x86.Build.0 = Release|Any CPU + {997AEFE5-D7D4-4033-A31A-07F476D6FE5D}.Debug.Appveyor|Any CPU.ActiveCfg = Debug|Any CPU + {997AEFE5-D7D4-4033-A31A-07F476D6FE5D}.Debug.Appveyor|Any CPU.Build.0 = Debug|Any CPU + {997AEFE5-D7D4-4033-A31A-07F476D6FE5D}.Debug.Appveyor|ARM.ActiveCfg = Debug|Any CPU + {997AEFE5-D7D4-4033-A31A-07F476D6FE5D}.Debug.Appveyor|ARM.Build.0 = Debug|Any CPU + {997AEFE5-D7D4-4033-A31A-07F476D6FE5D}.Debug.Appveyor|Mixed Platforms.ActiveCfg = Debug|Any CPU + {997AEFE5-D7D4-4033-A31A-07F476D6FE5D}.Debug.Appveyor|Mixed Platforms.Build.0 = Debug|Any CPU + {997AEFE5-D7D4-4033-A31A-07F476D6FE5D}.Debug.Appveyor|x64.ActiveCfg = Debug|Any CPU + {997AEFE5-D7D4-4033-A31A-07F476D6FE5D}.Debug.Appveyor|x64.Build.0 = Debug|Any CPU + {997AEFE5-D7D4-4033-A31A-07F476D6FE5D}.Debug.Appveyor|x86.ActiveCfg = Debug|Any CPU + {997AEFE5-D7D4-4033-A31A-07F476D6FE5D}.Debug.Appveyor|x86.Build.0 = Debug|Any CPU + {997AEFE5-D7D4-4033-A31A-07F476D6FE5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {997AEFE5-D7D4-4033-A31A-07F476D6FE5D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {997AEFE5-D7D4-4033-A31A-07F476D6FE5D}.Debug|ARM.ActiveCfg = Debug|Any CPU + {997AEFE5-D7D4-4033-A31A-07F476D6FE5D}.Debug|ARM.Build.0 = Debug|Any CPU + {997AEFE5-D7D4-4033-A31A-07F476D6FE5D}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {997AEFE5-D7D4-4033-A31A-07F476D6FE5D}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {997AEFE5-D7D4-4033-A31A-07F476D6FE5D}.Debug|x64.ActiveCfg = Debug|Any CPU + {997AEFE5-D7D4-4033-A31A-07F476D6FE5D}.Debug|x64.Build.0 = Debug|Any CPU + {997AEFE5-D7D4-4033-A31A-07F476D6FE5D}.Debug|x86.ActiveCfg = Debug|Any CPU + {997AEFE5-D7D4-4033-A31A-07F476D6FE5D}.Debug|x86.Build.0 = Debug|Any CPU + {997AEFE5-D7D4-4033-A31A-07F476D6FE5D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {997AEFE5-D7D4-4033-A31A-07F476D6FE5D}.Release|Any CPU.Build.0 = Release|Any CPU + {997AEFE5-D7D4-4033-A31A-07F476D6FE5D}.Release|ARM.ActiveCfg = Release|Any CPU + {997AEFE5-D7D4-4033-A31A-07F476D6FE5D}.Release|ARM.Build.0 = Release|Any CPU + {997AEFE5-D7D4-4033-A31A-07F476D6FE5D}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {997AEFE5-D7D4-4033-A31A-07F476D6FE5D}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {997AEFE5-D7D4-4033-A31A-07F476D6FE5D}.Release|x64.ActiveCfg = Release|Any CPU + {997AEFE5-D7D4-4033-A31A-07F476D6FE5D}.Release|x64.Build.0 = Release|Any CPU + {997AEFE5-D7D4-4033-A31A-07F476D6FE5D}.Release|x86.ActiveCfg = Release|Any CPU + {997AEFE5-D7D4-4033-A31A-07F476D6FE5D}.Release|x86.Build.0 = Release|Any CPU + {6D7D1D60-4473-4C52-800C-9B892C6640A5}.Debug.Appveyor|Any CPU.ActiveCfg = Debug|Any CPU + {6D7D1D60-4473-4C52-800C-9B892C6640A5}.Debug.Appveyor|Any CPU.Build.0 = Debug|Any CPU + {6D7D1D60-4473-4C52-800C-9B892C6640A5}.Debug.Appveyor|ARM.ActiveCfg = Debug|Any CPU + {6D7D1D60-4473-4C52-800C-9B892C6640A5}.Debug.Appveyor|ARM.Build.0 = Debug|Any CPU + {6D7D1D60-4473-4C52-800C-9B892C6640A5}.Debug.Appveyor|Mixed Platforms.ActiveCfg = Debug|Any CPU + {6D7D1D60-4473-4C52-800C-9B892C6640A5}.Debug.Appveyor|Mixed Platforms.Build.0 = Debug|Any CPU + {6D7D1D60-4473-4C52-800C-9B892C6640A5}.Debug.Appveyor|x64.ActiveCfg = Debug|Any CPU + {6D7D1D60-4473-4C52-800C-9B892C6640A5}.Debug.Appveyor|x64.Build.0 = Debug|Any CPU + {6D7D1D60-4473-4C52-800C-9B892C6640A5}.Debug.Appveyor|x86.ActiveCfg = Debug|Any CPU + {6D7D1D60-4473-4C52-800C-9B892C6640A5}.Debug.Appveyor|x86.Build.0 = Debug|Any CPU + {6D7D1D60-4473-4C52-800C-9B892C6640A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6D7D1D60-4473-4C52-800C-9B892C6640A5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6D7D1D60-4473-4C52-800C-9B892C6640A5}.Debug|ARM.ActiveCfg = Debug|Any CPU + {6D7D1D60-4473-4C52-800C-9B892C6640A5}.Debug|ARM.Build.0 = Debug|Any CPU + {6D7D1D60-4473-4C52-800C-9B892C6640A5}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {6D7D1D60-4473-4C52-800C-9B892C6640A5}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {6D7D1D60-4473-4C52-800C-9B892C6640A5}.Debug|x64.ActiveCfg = Debug|Any CPU + {6D7D1D60-4473-4C52-800C-9B892C6640A5}.Debug|x64.Build.0 = Debug|Any CPU + {6D7D1D60-4473-4C52-800C-9B892C6640A5}.Debug|x86.ActiveCfg = Debug|Any CPU + {6D7D1D60-4473-4C52-800C-9B892C6640A5}.Debug|x86.Build.0 = Debug|Any CPU + {6D7D1D60-4473-4C52-800C-9B892C6640A5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6D7D1D60-4473-4C52-800C-9B892C6640A5}.Release|Any CPU.Build.0 = Release|Any CPU + {6D7D1D60-4473-4C52-800C-9B892C6640A5}.Release|ARM.ActiveCfg = Release|Any CPU + {6D7D1D60-4473-4C52-800C-9B892C6640A5}.Release|ARM.Build.0 = Release|Any CPU + {6D7D1D60-4473-4C52-800C-9B892C6640A5}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {6D7D1D60-4473-4C52-800C-9B892C6640A5}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {6D7D1D60-4473-4C52-800C-9B892C6640A5}.Release|x64.ActiveCfg = Release|Any CPU + {6D7D1D60-4473-4C52-800C-9B892C6640A5}.Release|x64.Build.0 = Release|Any CPU + {6D7D1D60-4473-4C52-800C-9B892C6640A5}.Release|x86.ActiveCfg = Release|Any CPU + {6D7D1D60-4473-4C52-800C-9B892C6640A5}.Release|x86.Build.0 = Release|Any CPU + {E6D94C12-9AD7-46E6-AB62-3676F25FDE51}.Debug.Appveyor|Any CPU.ActiveCfg = Debug|Any CPU + {E6D94C12-9AD7-46E6-AB62-3676F25FDE51}.Debug.Appveyor|Any CPU.Build.0 = Debug|Any CPU + {E6D94C12-9AD7-46E6-AB62-3676F25FDE51}.Debug.Appveyor|ARM.ActiveCfg = Debug|Any CPU + {E6D94C12-9AD7-46E6-AB62-3676F25FDE51}.Debug.Appveyor|ARM.Build.0 = Debug|Any CPU + {E6D94C12-9AD7-46E6-AB62-3676F25FDE51}.Debug.Appveyor|Mixed Platforms.ActiveCfg = Debug|Any CPU + {E6D94C12-9AD7-46E6-AB62-3676F25FDE51}.Debug.Appveyor|Mixed Platforms.Build.0 = Debug|Any CPU + {E6D94C12-9AD7-46E6-AB62-3676F25FDE51}.Debug.Appveyor|x64.ActiveCfg = Debug|Any CPU + {E6D94C12-9AD7-46E6-AB62-3676F25FDE51}.Debug.Appveyor|x64.Build.0 = Debug|Any CPU + {E6D94C12-9AD7-46E6-AB62-3676F25FDE51}.Debug.Appveyor|x86.ActiveCfg = Debug|Any CPU + {E6D94C12-9AD7-46E6-AB62-3676F25FDE51}.Debug.Appveyor|x86.Build.0 = Debug|Any CPU + {E6D94C12-9AD7-46E6-AB62-3676F25FDE51}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E6D94C12-9AD7-46E6-AB62-3676F25FDE51}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E6D94C12-9AD7-46E6-AB62-3676F25FDE51}.Debug|ARM.ActiveCfg = Debug|Any CPU + {E6D94C12-9AD7-46E6-AB62-3676F25FDE51}.Debug|ARM.Build.0 = Debug|Any CPU + {E6D94C12-9AD7-46E6-AB62-3676F25FDE51}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {E6D94C12-9AD7-46E6-AB62-3676F25FDE51}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {E6D94C12-9AD7-46E6-AB62-3676F25FDE51}.Debug|x64.ActiveCfg = Debug|Any CPU + {E6D94C12-9AD7-46E6-AB62-3676F25FDE51}.Debug|x64.Build.0 = Debug|Any CPU + {E6D94C12-9AD7-46E6-AB62-3676F25FDE51}.Debug|x86.ActiveCfg = Debug|Any CPU + {E6D94C12-9AD7-46E6-AB62-3676F25FDE51}.Debug|x86.Build.0 = Debug|Any CPU + {E6D94C12-9AD7-46E6-AB62-3676F25FDE51}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E6D94C12-9AD7-46E6-AB62-3676F25FDE51}.Release|Any CPU.Build.0 = Release|Any CPU + {E6D94C12-9AD7-46E6-AB62-3676F25FDE51}.Release|ARM.ActiveCfg = Release|Any CPU + {E6D94C12-9AD7-46E6-AB62-3676F25FDE51}.Release|ARM.Build.0 = Release|Any CPU + {E6D94C12-9AD7-46E6-AB62-3676F25FDE51}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {E6D94C12-9AD7-46E6-AB62-3676F25FDE51}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {E6D94C12-9AD7-46E6-AB62-3676F25FDE51}.Release|x64.ActiveCfg = Release|Any CPU + {E6D94C12-9AD7-46E6-AB62-3676F25FDE51}.Release|x64.Build.0 = Release|Any CPU + {E6D94C12-9AD7-46E6-AB62-3676F25FDE51}.Release|x86.ActiveCfg = Release|Any CPU + {E6D94C12-9AD7-46E6-AB62-3676F25FDE51}.Release|x86.Build.0 = Release|Any CPU + {4A35B1C5-520D-4267-BA70-2DCEAC0A5662}.Debug.Appveyor|Any CPU.ActiveCfg = Debug|Any CPU + {4A35B1C5-520D-4267-BA70-2DCEAC0A5662}.Debug.Appveyor|Any CPU.Build.0 = Debug|Any CPU + {4A35B1C5-520D-4267-BA70-2DCEAC0A5662}.Debug.Appveyor|ARM.ActiveCfg = Debug|Any CPU + {4A35B1C5-520D-4267-BA70-2DCEAC0A5662}.Debug.Appveyor|ARM.Build.0 = Debug|Any CPU + {4A35B1C5-520D-4267-BA70-2DCEAC0A5662}.Debug.Appveyor|Mixed Platforms.ActiveCfg = Debug|Any CPU + {4A35B1C5-520D-4267-BA70-2DCEAC0A5662}.Debug.Appveyor|Mixed Platforms.Build.0 = Debug|Any CPU + {4A35B1C5-520D-4267-BA70-2DCEAC0A5662}.Debug.Appveyor|x64.ActiveCfg = Debug|Any CPU + {4A35B1C5-520D-4267-BA70-2DCEAC0A5662}.Debug.Appveyor|x64.Build.0 = Debug|Any CPU + {4A35B1C5-520D-4267-BA70-2DCEAC0A5662}.Debug.Appveyor|x86.ActiveCfg = Debug|Any CPU + {4A35B1C5-520D-4267-BA70-2DCEAC0A5662}.Debug.Appveyor|x86.Build.0 = Debug|Any CPU + {4A35B1C5-520D-4267-BA70-2DCEAC0A5662}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4A35B1C5-520D-4267-BA70-2DCEAC0A5662}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4A35B1C5-520D-4267-BA70-2DCEAC0A5662}.Debug|ARM.ActiveCfg = Debug|Any CPU + {4A35B1C5-520D-4267-BA70-2DCEAC0A5662}.Debug|ARM.Build.0 = Debug|Any CPU + {4A35B1C5-520D-4267-BA70-2DCEAC0A5662}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {4A35B1C5-520D-4267-BA70-2DCEAC0A5662}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {4A35B1C5-520D-4267-BA70-2DCEAC0A5662}.Debug|x64.ActiveCfg = Debug|Any CPU + {4A35B1C5-520D-4267-BA70-2DCEAC0A5662}.Debug|x64.Build.0 = Debug|Any CPU + {4A35B1C5-520D-4267-BA70-2DCEAC0A5662}.Debug|x86.ActiveCfg = Debug|Any CPU + {4A35B1C5-520D-4267-BA70-2DCEAC0A5662}.Debug|x86.Build.0 = Debug|Any CPU + {4A35B1C5-520D-4267-BA70-2DCEAC0A5662}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4A35B1C5-520D-4267-BA70-2DCEAC0A5662}.Release|Any CPU.Build.0 = Release|Any CPU + {4A35B1C5-520D-4267-BA70-2DCEAC0A5662}.Release|ARM.ActiveCfg = Release|Any CPU + {4A35B1C5-520D-4267-BA70-2DCEAC0A5662}.Release|ARM.Build.0 = Release|Any CPU + {4A35B1C5-520D-4267-BA70-2DCEAC0A5662}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {4A35B1C5-520D-4267-BA70-2DCEAC0A5662}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {4A35B1C5-520D-4267-BA70-2DCEAC0A5662}.Release|x64.ActiveCfg = Release|Any CPU + {4A35B1C5-520D-4267-BA70-2DCEAC0A5662}.Release|x64.Build.0 = Release|Any CPU + {4A35B1C5-520D-4267-BA70-2DCEAC0A5662}.Release|x86.ActiveCfg = Release|Any CPU + {4A35B1C5-520D-4267-BA70-2DCEAC0A5662}.Release|x86.Build.0 = Release|Any CPU + {2150E333-8FDC-42A3-9474-1A3956D46DE8}.Debug.Appveyor|Any CPU.ActiveCfg = Debug|Any CPU + {2150E333-8FDC-42A3-9474-1A3956D46DE8}.Debug.Appveyor|Any CPU.Build.0 = Debug|Any CPU + {2150E333-8FDC-42A3-9474-1A3956D46DE8}.Debug.Appveyor|ARM.ActiveCfg = Debug|Any CPU + {2150E333-8FDC-42A3-9474-1A3956D46DE8}.Debug.Appveyor|ARM.Build.0 = Debug|Any CPU + {2150E333-8FDC-42A3-9474-1A3956D46DE8}.Debug.Appveyor|Mixed Platforms.ActiveCfg = Debug|Any CPU + {2150E333-8FDC-42A3-9474-1A3956D46DE8}.Debug.Appveyor|Mixed Platforms.Build.0 = Debug|Any CPU + {2150E333-8FDC-42A3-9474-1A3956D46DE8}.Debug.Appveyor|x64.ActiveCfg = Debug|Any CPU + {2150E333-8FDC-42A3-9474-1A3956D46DE8}.Debug.Appveyor|x64.Build.0 = Debug|Any CPU + {2150E333-8FDC-42A3-9474-1A3956D46DE8}.Debug.Appveyor|x86.ActiveCfg = Debug|Any CPU + {2150E333-8FDC-42A3-9474-1A3956D46DE8}.Debug.Appveyor|x86.Build.0 = Debug|Any CPU + {2150E333-8FDC-42A3-9474-1A3956D46DE8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2150E333-8FDC-42A3-9474-1A3956D46DE8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2150E333-8FDC-42A3-9474-1A3956D46DE8}.Debug|ARM.ActiveCfg = Debug|Any CPU + {2150E333-8FDC-42A3-9474-1A3956D46DE8}.Debug|ARM.Build.0 = Debug|Any CPU + {2150E333-8FDC-42A3-9474-1A3956D46DE8}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {2150E333-8FDC-42A3-9474-1A3956D46DE8}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {2150E333-8FDC-42A3-9474-1A3956D46DE8}.Debug|x64.ActiveCfg = Debug|Any CPU + {2150E333-8FDC-42A3-9474-1A3956D46DE8}.Debug|x64.Build.0 = Debug|Any CPU + {2150E333-8FDC-42A3-9474-1A3956D46DE8}.Debug|x86.ActiveCfg = Debug|Any CPU + {2150E333-8FDC-42A3-9474-1A3956D46DE8}.Debug|x86.Build.0 = Debug|Any CPU + {2150E333-8FDC-42A3-9474-1A3956D46DE8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2150E333-8FDC-42A3-9474-1A3956D46DE8}.Release|Any CPU.Build.0 = Release|Any CPU + {2150E333-8FDC-42A3-9474-1A3956D46DE8}.Release|ARM.ActiveCfg = Release|Any CPU + {2150E333-8FDC-42A3-9474-1A3956D46DE8}.Release|ARM.Build.0 = Release|Any CPU + {2150E333-8FDC-42A3-9474-1A3956D46DE8}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {2150E333-8FDC-42A3-9474-1A3956D46DE8}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {2150E333-8FDC-42A3-9474-1A3956D46DE8}.Release|x64.ActiveCfg = Release|Any CPU + {2150E333-8FDC-42A3-9474-1A3956D46DE8}.Release|x64.Build.0 = Release|Any CPU + {2150E333-8FDC-42A3-9474-1A3956D46DE8}.Release|x86.ActiveCfg = Release|Any CPU + {2150E333-8FDC-42A3-9474-1A3956D46DE8}.Release|x86.Build.0 = Release|Any CPU + {E6D94FFD-7811-40BE-ABC4-6D6AB41F0060}.Debug.Appveyor|Any CPU.ActiveCfg = Debug|Any CPU + {E6D94FFD-7811-40BE-ABC4-6D6AB41F0060}.Debug.Appveyor|Any CPU.Build.0 = Debug|Any CPU + {E6D94FFD-7811-40BE-ABC4-6D6AB41F0060}.Debug.Appveyor|ARM.ActiveCfg = Debug|Any CPU + {E6D94FFD-7811-40BE-ABC4-6D6AB41F0060}.Debug.Appveyor|ARM.Build.0 = Debug|Any CPU + {E6D94FFD-7811-40BE-ABC4-6D6AB41F0060}.Debug.Appveyor|Mixed Platforms.ActiveCfg = Debug|Any CPU + {E6D94FFD-7811-40BE-ABC4-6D6AB41F0060}.Debug.Appveyor|Mixed Platforms.Build.0 = Debug|Any CPU + {E6D94FFD-7811-40BE-ABC4-6D6AB41F0060}.Debug.Appveyor|x64.ActiveCfg = Debug|Any CPU + {E6D94FFD-7811-40BE-ABC4-6D6AB41F0060}.Debug.Appveyor|x64.Build.0 = Debug|Any CPU + {E6D94FFD-7811-40BE-ABC4-6D6AB41F0060}.Debug.Appveyor|x86.ActiveCfg = Debug|Any CPU + {E6D94FFD-7811-40BE-ABC4-6D6AB41F0060}.Debug.Appveyor|x86.Build.0 = Debug|Any CPU + {E6D94FFD-7811-40BE-ABC4-6D6AB41F0060}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E6D94FFD-7811-40BE-ABC4-6D6AB41F0060}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E6D94FFD-7811-40BE-ABC4-6D6AB41F0060}.Debug|ARM.ActiveCfg = Debug|Any CPU + {E6D94FFD-7811-40BE-ABC4-6D6AB41F0060}.Debug|ARM.Build.0 = Debug|Any CPU + {E6D94FFD-7811-40BE-ABC4-6D6AB41F0060}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {E6D94FFD-7811-40BE-ABC4-6D6AB41F0060}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {E6D94FFD-7811-40BE-ABC4-6D6AB41F0060}.Debug|x64.ActiveCfg = Debug|Any CPU + {E6D94FFD-7811-40BE-ABC4-6D6AB41F0060}.Debug|x64.Build.0 = Debug|Any CPU + {E6D94FFD-7811-40BE-ABC4-6D6AB41F0060}.Debug|x86.ActiveCfg = Debug|Any CPU + {E6D94FFD-7811-40BE-ABC4-6D6AB41F0060}.Debug|x86.Build.0 = Debug|Any CPU + {E6D94FFD-7811-40BE-ABC4-6D6AB41F0060}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E6D94FFD-7811-40BE-ABC4-6D6AB41F0060}.Release|Any CPU.Build.0 = Release|Any CPU + {E6D94FFD-7811-40BE-ABC4-6D6AB41F0060}.Release|ARM.ActiveCfg = Release|Any CPU + {E6D94FFD-7811-40BE-ABC4-6D6AB41F0060}.Release|ARM.Build.0 = Release|Any CPU + {E6D94FFD-7811-40BE-ABC4-6D6AB41F0060}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {E6D94FFD-7811-40BE-ABC4-6D6AB41F0060}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {E6D94FFD-7811-40BE-ABC4-6D6AB41F0060}.Release|x64.ActiveCfg = Release|Any CPU + {E6D94FFD-7811-40BE-ABC4-6D6AB41F0060}.Release|x64.Build.0 = Release|Any CPU + {E6D94FFD-7811-40BE-ABC4-6D6AB41F0060}.Release|x86.ActiveCfg = Release|Any CPU + {E6D94FFD-7811-40BE-ABC4-6D6AB41F0060}.Release|x86.Build.0 = Release|Any CPU + {FE778406-ADCF-45A1-B775-A054B55BFC50}.Debug.Appveyor|Any CPU.ActiveCfg = Debug|Any CPU + {FE778406-ADCF-45A1-B775-A054B55BFC50}.Debug.Appveyor|Any CPU.Build.0 = Debug|Any CPU + {FE778406-ADCF-45A1-B775-A054B55BFC50}.Debug.Appveyor|ARM.ActiveCfg = Debug|Any CPU + {FE778406-ADCF-45A1-B775-A054B55BFC50}.Debug.Appveyor|ARM.Build.0 = Debug|Any CPU + {FE778406-ADCF-45A1-B775-A054B55BFC50}.Debug.Appveyor|Mixed Platforms.ActiveCfg = Debug|Any CPU + {FE778406-ADCF-45A1-B775-A054B55BFC50}.Debug.Appveyor|Mixed Platforms.Build.0 = Debug|Any CPU + {FE778406-ADCF-45A1-B775-A054B55BFC50}.Debug.Appveyor|x64.ActiveCfg = Debug|Any CPU + {FE778406-ADCF-45A1-B775-A054B55BFC50}.Debug.Appveyor|x64.Build.0 = Debug|Any CPU + {FE778406-ADCF-45A1-B775-A054B55BFC50}.Debug.Appveyor|x86.ActiveCfg = Debug|Any CPU + {FE778406-ADCF-45A1-B775-A054B55BFC50}.Debug.Appveyor|x86.Build.0 = Debug|Any CPU + {FE778406-ADCF-45A1-B775-A054B55BFC50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FE778406-ADCF-45A1-B775-A054B55BFC50}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FE778406-ADCF-45A1-B775-A054B55BFC50}.Debug|ARM.ActiveCfg = Debug|Any CPU + {FE778406-ADCF-45A1-B775-A054B55BFC50}.Debug|ARM.Build.0 = Debug|Any CPU + {FE778406-ADCF-45A1-B775-A054B55BFC50}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {FE778406-ADCF-45A1-B775-A054B55BFC50}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {FE778406-ADCF-45A1-B775-A054B55BFC50}.Debug|x64.ActiveCfg = Debug|Any CPU + {FE778406-ADCF-45A1-B775-A054B55BFC50}.Debug|x64.Build.0 = Debug|Any CPU + {FE778406-ADCF-45A1-B775-A054B55BFC50}.Debug|x86.ActiveCfg = Debug|Any CPU + {FE778406-ADCF-45A1-B775-A054B55BFC50}.Debug|x86.Build.0 = Debug|Any CPU + {FE778406-ADCF-45A1-B775-A054B55BFC50}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FE778406-ADCF-45A1-B775-A054B55BFC50}.Release|Any CPU.Build.0 = Release|Any CPU + {FE778406-ADCF-45A1-B775-A054B55BFC50}.Release|ARM.ActiveCfg = Release|Any CPU + {FE778406-ADCF-45A1-B775-A054B55BFC50}.Release|ARM.Build.0 = Release|Any CPU + {FE778406-ADCF-45A1-B775-A054B55BFC50}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {FE778406-ADCF-45A1-B775-A054B55BFC50}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {FE778406-ADCF-45A1-B775-A054B55BFC50}.Release|x64.ActiveCfg = Release|Any CPU + {FE778406-ADCF-45A1-B775-A054B55BFC50}.Release|x64.Build.0 = Release|Any CPU + {FE778406-ADCF-45A1-B775-A054B55BFC50}.Release|x86.ActiveCfg = Release|Any CPU + {FE778406-ADCF-45A1-B775-A054B55BFC50}.Release|x86.Build.0 = Release|Any CPU + {92A6F3CA-100F-4D9D-9742-B62267D445B6}.Debug.Appveyor|Any CPU.ActiveCfg = Debug|Any CPU + {92A6F3CA-100F-4D9D-9742-B62267D445B6}.Debug.Appveyor|Any CPU.Build.0 = Debug|Any CPU + {92A6F3CA-100F-4D9D-9742-B62267D445B6}.Debug.Appveyor|ARM.ActiveCfg = Debug|Any CPU + {92A6F3CA-100F-4D9D-9742-B62267D445B6}.Debug.Appveyor|ARM.Build.0 = Debug|Any CPU + {92A6F3CA-100F-4D9D-9742-B62267D445B6}.Debug.Appveyor|Mixed Platforms.ActiveCfg = Debug|Any CPU + {92A6F3CA-100F-4D9D-9742-B62267D445B6}.Debug.Appveyor|Mixed Platforms.Build.0 = Debug|Any CPU + {92A6F3CA-100F-4D9D-9742-B62267D445B6}.Debug.Appveyor|x64.ActiveCfg = Debug|Any CPU + {92A6F3CA-100F-4D9D-9742-B62267D445B6}.Debug.Appveyor|x64.Build.0 = Debug|Any CPU + {92A6F3CA-100F-4D9D-9742-B62267D445B6}.Debug.Appveyor|x86.ActiveCfg = Debug|Any CPU + {92A6F3CA-100F-4D9D-9742-B62267D445B6}.Debug.Appveyor|x86.Build.0 = Debug|Any CPU + {92A6F3CA-100F-4D9D-9742-B62267D445B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {92A6F3CA-100F-4D9D-9742-B62267D445B6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {92A6F3CA-100F-4D9D-9742-B62267D445B6}.Debug|ARM.ActiveCfg = Debug|Any CPU + {92A6F3CA-100F-4D9D-9742-B62267D445B6}.Debug|ARM.Build.0 = Debug|Any CPU + {92A6F3CA-100F-4D9D-9742-B62267D445B6}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {92A6F3CA-100F-4D9D-9742-B62267D445B6}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {92A6F3CA-100F-4D9D-9742-B62267D445B6}.Debug|x64.ActiveCfg = Debug|Any CPU + {92A6F3CA-100F-4D9D-9742-B62267D445B6}.Debug|x64.Build.0 = Debug|Any CPU + {92A6F3CA-100F-4D9D-9742-B62267D445B6}.Debug|x86.ActiveCfg = Debug|Any CPU + {92A6F3CA-100F-4D9D-9742-B62267D445B6}.Debug|x86.Build.0 = Debug|Any CPU + {92A6F3CA-100F-4D9D-9742-B62267D445B6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {92A6F3CA-100F-4D9D-9742-B62267D445B6}.Release|Any CPU.Build.0 = Release|Any CPU + {92A6F3CA-100F-4D9D-9742-B62267D445B6}.Release|ARM.ActiveCfg = Release|Any CPU + {92A6F3CA-100F-4D9D-9742-B62267D445B6}.Release|ARM.Build.0 = Release|Any CPU + {92A6F3CA-100F-4D9D-9742-B62267D445B6}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {92A6F3CA-100F-4D9D-9742-B62267D445B6}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {92A6F3CA-100F-4D9D-9742-B62267D445B6}.Release|x64.ActiveCfg = Release|Any CPU + {92A6F3CA-100F-4D9D-9742-B62267D445B6}.Release|x64.Build.0 = Release|Any CPU + {92A6F3CA-100F-4D9D-9742-B62267D445B6}.Release|x86.ActiveCfg = Release|Any CPU + {92A6F3CA-100F-4D9D-9742-B62267D445B6}.Release|x86.Build.0 = Release|Any CPU + {602FF788-E926-4404-B3A2-D6B778A5FFB7}.Debug.Appveyor|Any CPU.ActiveCfg = Debug|Any CPU + {602FF788-E926-4404-B3A2-D6B778A5FFB7}.Debug.Appveyor|Any CPU.Build.0 = Debug|Any CPU + {602FF788-E926-4404-B3A2-D6B778A5FFB7}.Debug.Appveyor|ARM.ActiveCfg = Debug|Any CPU + {602FF788-E926-4404-B3A2-D6B778A5FFB7}.Debug.Appveyor|ARM.Build.0 = Debug|Any CPU + {602FF788-E926-4404-B3A2-D6B778A5FFB7}.Debug.Appveyor|Mixed Platforms.ActiveCfg = Debug|Any CPU + {602FF788-E926-4404-B3A2-D6B778A5FFB7}.Debug.Appveyor|Mixed Platforms.Build.0 = Debug|Any CPU + {602FF788-E926-4404-B3A2-D6B778A5FFB7}.Debug.Appveyor|x64.ActiveCfg = Debug|Any CPU + {602FF788-E926-4404-B3A2-D6B778A5FFB7}.Debug.Appveyor|x64.Build.0 = Debug|Any CPU + {602FF788-E926-4404-B3A2-D6B778A5FFB7}.Debug.Appveyor|x86.ActiveCfg = Debug|Any CPU + {602FF788-E926-4404-B3A2-D6B778A5FFB7}.Debug.Appveyor|x86.Build.0 = Debug|Any CPU + {602FF788-E926-4404-B3A2-D6B778A5FFB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {602FF788-E926-4404-B3A2-D6B778A5FFB7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {602FF788-E926-4404-B3A2-D6B778A5FFB7}.Debug|ARM.ActiveCfg = Debug|Any CPU + {602FF788-E926-4404-B3A2-D6B778A5FFB7}.Debug|ARM.Build.0 = Debug|Any CPU + {602FF788-E926-4404-B3A2-D6B778A5FFB7}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {602FF788-E926-4404-B3A2-D6B778A5FFB7}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {602FF788-E926-4404-B3A2-D6B778A5FFB7}.Debug|x64.ActiveCfg = Debug|Any CPU + {602FF788-E926-4404-B3A2-D6B778A5FFB7}.Debug|x64.Build.0 = Debug|Any CPU + {602FF788-E926-4404-B3A2-D6B778A5FFB7}.Debug|x86.ActiveCfg = Debug|Any CPU + {602FF788-E926-4404-B3A2-D6B778A5FFB7}.Debug|x86.Build.0 = Debug|Any CPU + {602FF788-E926-4404-B3A2-D6B778A5FFB7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {602FF788-E926-4404-B3A2-D6B778A5FFB7}.Release|Any CPU.Build.0 = Release|Any CPU + {602FF788-E926-4404-B3A2-D6B778A5FFB7}.Release|ARM.ActiveCfg = Release|Any CPU + {602FF788-E926-4404-B3A2-D6B778A5FFB7}.Release|ARM.Build.0 = Release|Any CPU + {602FF788-E926-4404-B3A2-D6B778A5FFB7}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {602FF788-E926-4404-B3A2-D6B778A5FFB7}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {602FF788-E926-4404-B3A2-D6B778A5FFB7}.Release|x64.ActiveCfg = Release|Any CPU + {602FF788-E926-4404-B3A2-D6B778A5FFB7}.Release|x64.Build.0 = Release|Any CPU + {602FF788-E926-4404-B3A2-D6B778A5FFB7}.Release|x86.ActiveCfg = Release|Any CPU + {602FF788-E926-4404-B3A2-D6B778A5FFB7}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {B1C55C9B-3287-4EB2-8ADD-795DBC77013D} = {9051DDA0-E563-45D5-9504-085EBAACF469} + {AC3B3DDC-F011-4E19-8C9B-F748B19ED3C0} = {9051DDA0-E563-45D5-9504-085EBAACF469} + {4205A187-9732-4DA8-B0BE-77A2C6B8C6A1} = {8C7B43EB-2F93-483C-B433-E28F9386AD67} + {73896669-F05C-41AC-9F6F-A11F549EDEDC} = {9051DDA0-E563-45D5-9504-085EBAACF469} + {8BF81225-2F85-4412-AD18-6579CBA1879B} = {9051DDA0-E563-45D5-9504-085EBAACF469} + {997AEFE5-D7D4-4033-A31A-07F476D6FE5D} = {1C42C435-8826-4044-8775-A1DA40EF4866} + {6D7D1D60-4473-4C52-800C-9B892C6640A5} = {9051DDA0-E563-45D5-9504-085EBAACF469} + {E6D94C12-9AD7-46E6-AB62-3676F25FDE51} = {9051DDA0-E563-45D5-9504-085EBAACF469} + {4A35B1C5-520D-4267-BA70-2DCEAC0A5662} = {8C7B43EB-2F93-483C-B433-E28F9386AD67} + {2150E333-8FDC-42A3-9474-1A3956D46DE8} = {8C7B43EB-2F93-483C-B433-E28F9386AD67} + {E6D94FFD-7811-40BE-ABC4-6D6AB41F0060} = {9051DDA0-E563-45D5-9504-085EBAACF469} + {FE778406-ADCF-45A1-B775-A054B55BFC50} = {55B8F371-B2BA-4DEE-AB98-5BAB8A21B1C2} + {602FF788-E926-4404-B3A2-D6B778A5FFB7} = {9051DDA0-E563-45D5-9504-085EBAACF469} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {77FF357B-03FA-4FA5-A68F-BFBE5800FEBA} + EndGlobalSection EndGlobal diff --git a/RestSharp.sln.DotSettings b/RestSharp.sln.DotSettings new file mode 100644 index 000000000..d0bb9d525 --- /dev/null +++ b/RestSharp.sln.DotSettings @@ -0,0 +1,113 @@ + + False + SUGGESTION + SUGGESTION + SUGGESTION + <?xml version="1.0" encoding="utf-16"?><Profile name="Apptrium Code Format"><JsReformatCode>True</JsReformatCode><CSCodeStyleAttributes ArrangeTypeAccessModifier="True" ArrangeTypeMemberAccessModifier="True" SortModifiers="True" RemoveRedundantParentheses="True" AddMissingParentheses="True" ArrangeBraces="True" ArrangeAttributes="True" ArrangeArgumentsStyle="True" ArrangeCodeBodyStyle="True" ArrangeVarStyle="True" /><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings><EmbraceInRegion>False</EmbraceInRegion><RegionName></RegionName></CSOptimizeUsings><CSReformatCode>True</CSReformatCode><XAMLCollapseEmptyTags>False</XAMLCollapseEmptyTags><CSReorderTypeMembers>True</CSReorderTypeMembers><CSArrangeQualifiers>True</CSArrangeQualifiers><CSShortenReferences>True</CSShortenReferences><IDEA_SETTINGS>&lt;profile version="1.0"&gt; + &lt;option name="myName" value="Apptrium Code Format" /&gt; + &lt;inspection_tool class="AccessStaticViaInstance" enabled="false" level="WARNING" enabled_by_default="false" /&gt; + &lt;inspection_tool class="CharsetObjectCanBeUsed" enabled="false" level="WARNING" enabled_by_default="false" /&gt; + &lt;inspection_tool class="DuplicateThrows" enabled="false" level="WARNING" enabled_by_default="false" /&gt; + &lt;inspection_tool class="ES6ShorthandObjectProperty" enabled="false" level="INFORMATION" enabled_by_default="false" /&gt; + &lt;inspection_tool class="JSArrowFunctionBracesCanBeRemoved" enabled="false" level="INFORMATION" enabled_by_default="false" /&gt; + &lt;inspection_tool class="JSPrimitiveTypeWrapperUsage" enabled="false" level="WARNING" enabled_by_default="false" /&gt; + &lt;inspection_tool class="JSRemoveUnnecessaryParentheses" enabled="false" level="INFORMATION" enabled_by_default="false" /&gt; + &lt;inspection_tool class="JSUnnecessarySemicolon" enabled="false" level="WARNING" enabled_by_default="false" /&gt; + &lt;inspection_tool class="RedundantArrayCreation" enabled="false" level="WARNING" enabled_by_default="false" /&gt; + &lt;inspection_tool class="RedundantCast" enabled="false" level="WARNING" enabled_by_default="false" /&gt; + &lt;inspection_tool class="RedundantTypeArguments" enabled="false" level="WARNING" enabled_by_default="false" /&gt; + &lt;inspection_tool class="TypescriptExplicitMemberType" enabled="false" level="INFORMATION" enabled_by_default="false" /&gt; + &lt;inspection_tool class="UnnecessaryContinueJS" enabled="false" level="WARNING" enabled_by_default="false" /&gt; + &lt;inspection_tool class="UnnecessaryLabelJS" enabled="false" level="WARNING" enabled_by_default="false" /&gt; + &lt;inspection_tool class="UnnecessaryLabelOnBreakStatementJS" enabled="false" level="WARNING" enabled_by_default="false" /&gt; + &lt;inspection_tool class="UnnecessaryLabelOnContinueStatementJS" enabled="false" level="WARNING" enabled_by_default="false" /&gt; + &lt;inspection_tool class="UnnecessaryReturnJS" enabled="false" level="WARNING" enabled_by_default="false" /&gt; + &lt;inspection_tool class="UnterminatedStatementJS" enabled="false" level="WARNING" enabled_by_default="false" /&gt; +&lt;/profile&gt;</IDEA_SETTINGS><CSMakeAutoPropertyGetOnly>True</CSMakeAutoPropertyGetOnly><CSMakeFieldReadonly>True</CSMakeFieldReadonly><CSUseAutoProperty>True</CSUseAutoProperty><RemoveCodeRedundancies>True</RemoveCodeRedundancies><VBReformatCode>True</VBReformatCode><CssReformatCode>True</CssReformatCode><HtmlReformatCode>True</HtmlReformatCode></Profile> + RequiredForMultiline + RequiredForMultiline + RequiredForMultiline + ExpressionBody + Join + ExpressionBody + ExpressionBody + False + 1 + 0 + 0 + 1 + 0 + 1 + TOGETHER_SAME_LINE + True + True + False + True + 1 + 1 + False + 6 + NEVER + ALWAYS + IF_OWNER_IS_SINGLE_LINE + ALWAYS + IF_OWNER_IS_SINGLE_LINE + True + True + True + True + True + True + True + CHOP_IF_LONG + WRAP_IF_LONG + 150 + CHOP_ALWAYS + CHOP_IF_LONG + False + FDIC + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb_AaBb" /> + <Policy><Descriptor Staticness="Instance" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Instance fields (not private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb_AaBb" /></Policy> + <Policy><Descriptor Staticness="Static" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Static fields (not private)"><ElementKinds><Kind Name="FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb_AaBb" /></Policy> + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + 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. + + True + True + \ No newline at end of file diff --git a/RestSharp.snk b/RestSharp.snk new file mode 100644 index 000000000..493c9d347 Binary files /dev/null and b/RestSharp.snk differ diff --git a/RestSharp/Authenticators/HttpBasicAuthenticator.cs b/RestSharp/Authenticators/HttpBasicAuthenticator.cs deleted file mode 100644 index 272faba16..000000000 --- a/RestSharp/Authenticators/HttpBasicAuthenticator.cs +++ /dev/null @@ -1,50 +0,0 @@ -#region License -// Copyright 2010 John Sheehan -// -// 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. -#endregion - -using System; -using System.Linq; -using System.Text; - -namespace RestSharp -{ - public class HttpBasicAuthenticator : IAuthenticator - { - private readonly string _username; - private readonly string _password; - - public HttpBasicAuthenticator(string username, string password) { - _password = password; - _username = username; - } - - public void Authenticate(IRestClient client, IRestRequest request) { - // NetworkCredentials always makes two trips, even if with PreAuthenticate, - // it is also unsafe for many partial trust scenarios - // request.Credentials = Credentials; - // thanks TweetSharp! - - // request.Credentials = new NetworkCredential(_username, _password); - - // only add the Authorization parameter if it hasn't been added by a previous Execute - if (!request.Parameters.Any(p => p.Name.Equals("Authorization", StringComparison.OrdinalIgnoreCase))) - { - var token = Convert.ToBase64String(Encoding.UTF8.GetBytes(string.Format("{0}:{1}", _username, _password))); - var authHeader = string.Format("Basic {0}", token); - request.AddParameter("Authorization", authHeader, ParameterType.HttpHeader); - } - } - } -} diff --git a/RestSharp/Authenticators/NtlmAuthenticator.cs b/RestSharp/Authenticators/NtlmAuthenticator.cs deleted file mode 100644 index 6474ab4b7..000000000 --- a/RestSharp/Authenticators/NtlmAuthenticator.cs +++ /dev/null @@ -1,65 +0,0 @@ -#region License -// Copyright 2010 John Sheehan -// -// 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. -#endregion - -using System; -using System.Net; - -#if FRAMEWORK - -namespace RestSharp -{ - /// - /// Tries to Authenticate with the credentials of the currently logged in user, or impersonate a user - /// - public class NtlmAuthenticator : IAuthenticator - { - private readonly ICredentials credentials; - - /// - /// Authenticate with the credentials of the currently logged in user - /// - public NtlmAuthenticator() - : this(CredentialCache.DefaultCredentials) - { - } - - /// - /// Authenticate by impersonation - /// - /// - /// - public NtlmAuthenticator(string username, string password) : this(new NetworkCredential(username, password)) - { - } - - /// - /// Authenticate by impersonation, using an existing ICredentials instance - /// - /// - public NtlmAuthenticator(ICredentials credentials) - { - if (credentials == null) throw new ArgumentNullException("credentials"); - this.credentials = credentials; - } - - public void Authenticate(IRestClient client, IRestRequest request) - { - request.Credentials = credentials; - } - } -} - -#endif \ No newline at end of file diff --git a/RestSharp/Authenticators/OAuth/Extensions/CollectionExtensions.cs b/RestSharp/Authenticators/OAuth/Extensions/CollectionExtensions.cs deleted file mode 100644 index 9efadeb89..000000000 --- a/RestSharp/Authenticators/OAuth/Extensions/CollectionExtensions.cs +++ /dev/null @@ -1,110 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Text; - -namespace RestSharp.Authenticators.OAuth.Extensions -{ - internal static class CollectionExtensions - { - public static IEnumerable AsEnumerable(this T item) - { - return new[] {item}; - } - - public static IEnumerable And(this T item, T other) - { - return new[] {item, other}; - } - - public static IEnumerable And(this IEnumerable items, T item) - { - foreach (var i in items) - { - yield return i; - } - - yield return item; - } - - public static K TryWithKey(this IDictionary dictionary, T key) - { - return dictionary.ContainsKey(key) ? dictionary[key] : default(K); - } - - public static IEnumerable ToEnumerable(this object[] items) where T : class - { - foreach (var item in items) - { - var record = item as T; - yield return record; - } - } - - public static void ForEach(this IEnumerable items, Action action) - { - foreach (var item in items) - { - action(item); - } - } - -#if !WINDOWS_PHONE && !SILVERLIGHT && !PocketPC - - public static void AddRange(this IDictionary collection, NameValueCollection range) - { - foreach (var key in range.AllKeys) - { - collection.Add(key, range[key]); - } - } - - public static string ToQueryString(this NameValueCollection collection) - { - var sb = new StringBuilder(); - if (collection.Count > 0) - { - sb.Append("?"); - } - - var count = 0; - foreach (var key in collection.AllKeys) - { - sb.AppendFormat("{0}={1}", key, collection[key].UrlEncode()); - count++; - - if (count >= collection.Count) - { - continue; - } - sb.Append("&"); - } - return sb.ToString(); - } - -#endif - - public static string Concatenate(this WebParameterCollection collection, string separator, string spacer) - { - var sb = new StringBuilder(); - - var total = collection.Count; - var count = 0; - - foreach (var item in collection) - { - sb.Append(item.Name); - sb.Append(separator); - sb.Append(item.Value); - - count++; - if (count < total) - { - sb.Append(spacer); - } - } - - return sb.ToString(); - } - } -} \ No newline at end of file diff --git a/RestSharp/Authenticators/OAuth/Extensions/OAuthExtensions.cs b/RestSharp/Authenticators/OAuth/Extensions/OAuthExtensions.cs deleted file mode 100644 index f87c2609a..000000000 --- a/RestSharp/Authenticators/OAuth/Extensions/OAuthExtensions.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Security.Cryptography; -using System.Text; - -namespace RestSharp.Authenticators.OAuth.Extensions -{ - internal static class OAuthExtensions - { - public static string ToRequestValue(this OAuthSignatureMethod signatureMethod) - { - var value = signatureMethod.ToString().ToUpper(); - var shaIndex = value.IndexOf("SHA1"); - return shaIndex > -1 ? value.Insert(shaIndex, "-") : value; - } - - public static OAuthSignatureMethod FromRequestValue(this string signatureMethod) - { - switch (signatureMethod) - { - case "HMAC-SHA1": - return OAuthSignatureMethod.HmacSha1; - case "RSA-SHA1": - return OAuthSignatureMethod.RsaSha1; - default: - return OAuthSignatureMethod.PlainText; - } - } - - public static string HashWith(this string input, HashAlgorithm algorithm) - { - var data = Encoding.UTF8.GetBytes(input); - var hash = algorithm.ComputeHash(data); - return Convert.ToBase64String(hash); - } - } -} \ No newline at end of file diff --git a/RestSharp/Authenticators/OAuth/Extensions/StringExtensions.cs b/RestSharp/Authenticators/OAuth/Extensions/StringExtensions.cs deleted file mode 100644 index 4dd7ae3f9..000000000 --- a/RestSharp/Authenticators/OAuth/Extensions/StringExtensions.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; - -namespace RestSharp.Authenticators.OAuth.Extensions -{ - internal static class StringExtensions - { - public static bool IsNullOrBlank(this string value) - { - return String.IsNullOrEmpty(value) || - (!String.IsNullOrEmpty(value) && value.Trim() == String.Empty); - } - - public static bool EqualsIgnoreCase(this string left, string right) - { - return String.Compare(left, right, StringComparison.OrdinalIgnoreCase) == 0; - } - - public static bool EqualsAny(this string input, params string[] args) - { - return args.Aggregate(false, (current, arg) => current | input.Equals(arg)); - } - - public static string FormatWith(this string format, params object[] args) - { - return String.Format(format, args); - } - - public static string FormatWithInvariantCulture(this string format, params object[] args) - { - return String.Format(CultureInfo.InvariantCulture, format, args); - } - - public static string Then(this string input, string value) - { - return String.Concat(input, value); - } - - public static string UrlEncode(this string value) - { - // [DC] This is more correct than HttpUtility; it escapes spaces as %20, not + - return Uri.EscapeDataString(value); - } - - public static string UrlDecode(this string value) - { - return Uri.UnescapeDataString(value); - } - - public static Uri AsUri(this string value) - { - return new Uri(value); - } - - public static string ToBase64String(this byte[] input) - { - return Convert.ToBase64String(input); - } - - public static byte[] GetBytes(this string input) - { - return Encoding.UTF8.GetBytes(input); - } - - public static string PercentEncode(this string s) - { - var bytes = s.GetBytes(); - var sb = new StringBuilder(); - foreach (var b in bytes) - { - // [DC]: Support proper encoding of special characters (\n\r\t\b) - if ((b > 7 && b < 11) || b == 13) - { - sb.Append(string.Format("%0{0:X}", b)); - } - else - { - sb.Append(string.Format("%{0:X}", b)); - } - } - return sb.ToString(); - } - - public static IDictionary ParseQueryString(this string query) - { - // [DC]: This method does not URL decode, and cannot handle decoded input - if (query.StartsWith("?")) query = query.Substring(1); - - if (query.Equals(string.Empty)) - { - return new Dictionary(); - } - - var parts = query.Split(new[] {'&'}); - - return parts.Select( - part => part.Split(new[] {'='})).ToDictionary( - pair => pair[0], pair => pair[1] - ); - } - - private const RegexOptions Options = -#if !WINDOWS_PHONE && !SILVERLIGHT && !PocketPC - RegexOptions.Compiled | RegexOptions.IgnoreCase; -#else - RegexOptions.IgnoreCase; -#endif - } -} \ No newline at end of file diff --git a/RestSharp/Authenticators/OAuth/Extensions/TimeExtensions.cs b/RestSharp/Authenticators/OAuth/Extensions/TimeExtensions.cs deleted file mode 100644 index 1c06cc81e..000000000 --- a/RestSharp/Authenticators/OAuth/Extensions/TimeExtensions.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; - -namespace RestSharp.Authenticators.OAuth.Extensions -{ - internal static class TimeExtensions - { - public static DateTime FromNow(this TimeSpan value) - { - return new DateTime((DateTime.Now + value).Ticks); - } - - public static DateTime FromUnixTime(this long seconds) - { - var time = new DateTime(1970, 1, 1); - time = time.AddSeconds(seconds); - - return time.ToLocalTime(); - } - - public static long ToUnixTime(this DateTime dateTime) - { - var timeSpan = (dateTime - new DateTime(1970, 1, 1)); - var timestamp = (long) timeSpan.TotalSeconds; - - return timestamp; - } - } -} \ No newline at end of file diff --git a/RestSharp/Authenticators/OAuth/HttpPostParameter.cs b/RestSharp/Authenticators/OAuth/HttpPostParameter.cs deleted file mode 100644 index 0c930b18c..000000000 --- a/RestSharp/Authenticators/OAuth/HttpPostParameter.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.IO; - -namespace RestSharp.Authenticators.OAuth -{ - internal class HttpPostParameter : WebParameter - { - public HttpPostParameter(string name, string value) : base(name, value) - { - } - - public virtual HttpPostParameterType Type { get; private set; } - public virtual string FileName { get; private set; } - public virtual string FilePath { get; private set; } - public virtual Stream FileStream { get; set; } - public virtual string ContentType { get; private set; } - - public static HttpPostParameter CreateFile(string name, string fileName, string filePath, string contentType) - { - var parameter = new HttpPostParameter(name, string.Empty) - { - Type = HttpPostParameterType.File, - FileName = fileName, - FilePath = filePath, - ContentType = contentType, - }; - return parameter; - } - - public static HttpPostParameter CreateFile(string name, string fileName, Stream fileStream, string contentType) - { - var parameter = new HttpPostParameter(name, string.Empty) - { - Type = HttpPostParameterType.File, - FileName = fileName, - FileStream = fileStream, - ContentType = contentType, - }; - - return parameter; - } - } -} \ No newline at end of file diff --git a/RestSharp/Authenticators/OAuth/HttpPostParameterType.cs b/RestSharp/Authenticators/OAuth/HttpPostParameterType.cs deleted file mode 100644 index 7dbce9a92..000000000 --- a/RestSharp/Authenticators/OAuth/HttpPostParameterType.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; - -namespace RestSharp.Authenticators.OAuth -{ -#if !SILVERLIGHT && !WINDOWS_PHONE && !PocketPC - [Serializable] -#endif - internal enum HttpPostParameterType - { - Field, - File - } -} \ No newline at end of file diff --git a/RestSharp/Authenticators/OAuth/OAuthParameterHandling.cs b/RestSharp/Authenticators/OAuth/OAuthParameterHandling.cs deleted file mode 100644 index 3a7428c9a..000000000 --- a/RestSharp/Authenticators/OAuth/OAuthParameterHandling.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; - -namespace RestSharp.Authenticators.OAuth -{ -#if !SILVERLIGHT && !WINDOWS_PHONE && !PocketPC - [Serializable] -#endif - public enum OAuthParameterHandling - { - HttpAuthorizationHeader, - UrlOrPostParameters - } -} \ No newline at end of file diff --git a/RestSharp/Authenticators/OAuth/OAuthSignatureMethod.cs b/RestSharp/Authenticators/OAuth/OAuthSignatureMethod.cs deleted file mode 100644 index 47d9266f1..000000000 --- a/RestSharp/Authenticators/OAuth/OAuthSignatureMethod.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; - -namespace RestSharp.Authenticators.OAuth -{ -#if !SILVERLIGHT && !WINDOWS_PHONE && !PocketPC - [Serializable] -#endif - public enum OAuthSignatureMethod - { - HmacSha1, - PlainText, - RsaSha1 - } -} \ No newline at end of file diff --git a/RestSharp/Authenticators/OAuth/OAuthSignatureTreatment.cs b/RestSharp/Authenticators/OAuth/OAuthSignatureTreatment.cs deleted file mode 100644 index 3253a75ae..000000000 --- a/RestSharp/Authenticators/OAuth/OAuthSignatureTreatment.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; - -namespace RestSharp.Authenticators.OAuth -{ -#if !SILVERLIGHT && !WINDOWS_PHONE && !PocketPC - [Serializable] -#endif - public enum OAuthSignatureTreatment - { - Escaped, - Unescaped - } -} \ No newline at end of file diff --git a/RestSharp/Authenticators/OAuth/OAuthTools.cs b/RestSharp/Authenticators/OAuth/OAuthTools.cs deleted file mode 100644 index 0c6b421c3..000000000 --- a/RestSharp/Authenticators/OAuth/OAuthTools.cs +++ /dev/null @@ -1,341 +0,0 @@ -using System; -using System.Linq; -using System.Security.Cryptography; -using System.Text; -using RestSharp.Authenticators.OAuth.Extensions; - -namespace RestSharp.Authenticators.OAuth -{ -#if !SILVERLIGHT && !WINDOWS_PHONE && !PocketPC - [Serializable] -#endif - internal static class OAuthTools - { - private const string AlphaNumeric = Upper + Lower + Digit; - private const string Digit = "1234567890"; - private const string Lower = "abcdefghijklmnopqrstuvwxyz"; - private const string Unreserved = AlphaNumeric + "-._~"; - private const string Upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; - - private static readonly Random _random; - private static readonly object _randomLock = new object(); - -#if !SILVERLIGHT && !WINDOWS_PHONE && !PocketPC - private static readonly RandomNumberGenerator _rng = RandomNumberGenerator.Create(); -#endif - - static OAuthTools() - { -#if !SILVERLIGHT && !WINDOWS_PHONE && !PocketPC - var bytes = new byte[4]; - _rng.GetNonZeroBytes(bytes); - _random = new Random(BitConverter.ToInt32(bytes, 0)); -#else - _random = new Random(); -#endif - } - - /// - /// All text parameters are UTF-8 encoded (per section 5.1). - /// - /// - private static readonly Encoding _encoding = Encoding.UTF8; - - /// - /// Generates a random 16-byte lowercase alphanumeric string. - /// - /// - /// - public static string GetNonce() - { - const string chars = (Lower + Digit); - - var nonce = new char[16]; - lock (_randomLock) - { - for (var i = 0; i < nonce.Length; i++) - { - nonce[i] = chars[_random.Next(0, chars.Length)]; - } - } - return new string(nonce); - } - - /// - /// Generates a timestamp based on the current elapsed seconds since '01/01/1970 0000 GMT" - /// - /// - /// - public static string GetTimestamp() - { - return GetTimestamp(DateTime.UtcNow); - } - - /// - /// Generates a timestamp based on the elapsed seconds of a given time since '01/01/1970 0000 GMT" - /// - /// - /// A specified point in time. - /// - public static string GetTimestamp(DateTime dateTime) - { - var timestamp = dateTime.ToUnixTime(); - return timestamp.ToString(); - } - - /// - /// The set of characters that are unreserved in RFC 2396 but are NOT unreserved in RFC 3986. - /// - /// - private static readonly string[] UriRfc3986CharsToEscape = new[] { "!", "*", "'", "(", ")" }; - - private static readonly string[] UriRfc3968EscapedHex = new[] {"%21", "%2A", "%27", "%28", "%29"}; - - /// - /// URL encodes a string based on section 5.1 of the OAuth spec. - /// Namely, percent encoding with [RFC3986], avoiding unreserved characters, - /// upper-casing hexadecimal characters, and UTF-8 encoding for text value pairs. - /// - /// The value to escape. - /// The escaped value. - /// - /// The method is supposed to take on - /// RFC 3986 behavior if certain elements are present in a .config file. Even if this - /// actually worked (which in my experiments it doesn't), we can't rely on every - /// host actually having this configuration element present. - /// - /// - /// - public static string UrlEncodeRelaxed(string value) - { - // Start with RFC 2396 escaping by calling the .NET method to do the work. - // This MAY sometimes exhibit RFC 3986 behavior (according to the documentation). - // If it does, the escaping we do that follows it will be a no-op since the - // characters we search for to replace can't possibly exist in the string. - StringBuilder escaped = new StringBuilder(Uri.EscapeDataString(value)); - - // Upgrade the escaping to RFC 3986, if necessary. - for (int i = 0; i < UriRfc3986CharsToEscape.Length; i++) - { - string t = UriRfc3986CharsToEscape[i]; - escaped.Replace(t, UriRfc3968EscapedHex[i]); - } - - // Return the fully-RFC3986-escaped string. - return escaped.ToString(); - } - - /// - /// URL encodes a string based on section 5.1 of the OAuth spec. - /// Namely, percent encoding with [RFC3986], avoiding unreserved characters, - /// upper-casing hexadecimal characters, and UTF-8 encoding for text value pairs. - /// - /// - /// - public static string UrlEncodeStrict(string value) - { - // [JD]: We need to escape the apostrophe as well or the signature will fail - var original = value; - var ret = original.Where( - c => !Unreserved.Contains(c) && c != '%').Aggregate( - value, (current, c) => current.Replace( - c.ToString(), c.ToString().PercentEncode() - )); - - return ret.Replace("%%", "%25%"); // Revisit to encode actual %'s - } - - /// - /// Sorts a collection of key-value pairs by name, and then value if equal, - /// concatenating them into a single string. This string should be encoded - /// prior to, or after normalization is run. - /// - /// - /// - /// - public static string NormalizeRequestParameters(WebParameterCollection parameters) - { - var copy = SortParametersExcludingSignature(parameters); - var concatenated = copy.Concatenate("=", "&"); - return concatenated; - } - - /// - /// Sorts a by name, and then value if equal. - /// - /// A collection of parameters to sort - /// A sorted parameter collection - public static WebParameterCollection SortParametersExcludingSignature(WebParameterCollection parameters) - { - var copy = new WebParameterCollection(parameters); - var exclusions = copy.Where(n => n.Name.EqualsIgnoreCase("oauth_signature")); - - copy.RemoveAll(exclusions); - copy.ForEach(p => { p.Name = UrlEncodeStrict(p.Name); p.Value = UrlEncodeStrict(p.Value); }); - copy.Sort( - (x, y) => - string.CompareOrdinal(x.Name, y.Name) != 0 - ? string.CompareOrdinal(x.Name, y.Name) - : string.CompareOrdinal(x.Value, y.Value)); - return copy; - } - - /// - /// Creates a request URL suitable for making OAuth requests. - /// Resulting URLs must exclude port 80 or port 443 when accompanied by HTTP and HTTPS, respectively. - /// Resulting URLs must be lower case. - /// - /// - /// The original request URL - /// - public static string ConstructRequestUrl(Uri url) - { - if (url == null) - { - throw new ArgumentNullException("url"); - } - - var sb = new StringBuilder(); - - var requestUrl = "{0}://{1}".FormatWith(url.Scheme, url.Host); - var qualified = ":{0}".FormatWith(url.Port); - var basic = url.Scheme == "http" && url.Port == 80; - var secure = url.Scheme == "https" && url.Port == 443; - - sb.Append(requestUrl); - sb.Append(!basic && !secure ? qualified : ""); - sb.Append(url.AbsolutePath); - - return sb.ToString(); //.ToLower(); - } - - /// - /// Creates a request elements concatentation value to send with a request. - /// This is also known as the signature base. - /// - /// - /// - /// The request's HTTP method type - /// The request URL - /// The request's parameters - /// A signature base string - public static string ConcatenateRequestElements(string method, string url, WebParameterCollection parameters) - { - var sb = new StringBuilder(); - - // Separating &'s are not URL encoded - var requestMethod = method.ToUpper().Then("&"); - var requestUrl = UrlEncodeRelaxed(ConstructRequestUrl(url.AsUri())).Then("&"); - var requestParameters = UrlEncodeRelaxed(NormalizeRequestParameters(parameters)); - - sb.Append(requestMethod); - sb.Append(requestUrl); - sb.Append(requestParameters); - - return sb.ToString(); - } - - /// - /// Creates a signature value given a signature base and the consumer secret. - /// This method is used when the token secret is currently unknown. - /// - /// - /// The hashing method - /// The signature base - /// The consumer key - /// - public static string GetSignature(OAuthSignatureMethod signatureMethod, string signatureBase, string consumerSecret) - { - return GetSignature(signatureMethod, OAuthSignatureTreatment.Escaped, signatureBase, consumerSecret, null); - } - - /// - /// Creates a signature value given a signature base and the consumer secret. - /// This method is used when the token secret is currently unknown. - /// - /// - /// The hashing method - /// The treatment to use on a signature value - /// The signature base - /// The consumer key - /// - public static string GetSignature(OAuthSignatureMethod signatureMethod, OAuthSignatureTreatment signatureTreatment, string signatureBase, string consumerSecret) - { - return GetSignature(signatureMethod, signatureTreatment, signatureBase, consumerSecret, null); - } - - /// - /// Creates a signature value given a signature base and the consumer secret and a known token secret. - /// - /// - /// The hashing method - /// The signature base - /// The consumer secret - /// The token secret - /// - public static string GetSignature(OAuthSignatureMethod signatureMethod, string signatureBase, string consumerSecret, string tokenSecret) - { - return GetSignature(signatureMethod, OAuthSignatureTreatment.Escaped, consumerSecret, tokenSecret); - } - - /// - /// Creates a signature value given a signature base and the consumer secret and a known token secret. - /// - /// - /// The hashing method - /// The treatment to use on a signature value - /// The signature base - /// The consumer secret - /// The token secret - /// - public static string GetSignature(OAuthSignatureMethod signatureMethod, - OAuthSignatureTreatment signatureTreatment, - string signatureBase, - string consumerSecret, - string tokenSecret) - { - if (tokenSecret.IsNullOrBlank()) - { - tokenSecret = String.Empty; - } - - consumerSecret = UrlEncodeRelaxed(consumerSecret); - tokenSecret = UrlEncodeRelaxed(tokenSecret); - - string signature; - switch (signatureMethod) - { -#if !PocketPC - case OAuthSignatureMethod.HmacSha1: - { - var crypto = new HMACSHA1(); - var key = "{0}&{1}".FormatWith(consumerSecret, tokenSecret); - - crypto.Key = _encoding.GetBytes(key); - signature = signatureBase.HashWith(crypto); - - break; - } -#endif - case OAuthSignatureMethod.PlainText: - { - signature = "{0}&{1}".FormatWith(consumerSecret, tokenSecret); - - break; - } - default: -#if PocketPC - throw new NotImplementedException("Only PlainText is currently supported."); -#else - throw new NotImplementedException("Only HMAC-SHA1 is currently supported."); -#endif - } - - var result = signatureTreatment == OAuthSignatureTreatment.Escaped - ? UrlEncodeRelaxed(signature) - : signature; - - return result; - } - } -} \ No newline at end of file diff --git a/RestSharp/Authenticators/OAuth/OAuthType.cs b/RestSharp/Authenticators/OAuth/OAuthType.cs deleted file mode 100644 index 96b05d624..000000000 --- a/RestSharp/Authenticators/OAuth/OAuthType.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace RestSharp.Authenticators.OAuth -{ -#if !SILVERLIGHT && !WINDOWS_PHONE && !PocketPC - [Serializable] -#endif - public enum OAuthType - { - RequestToken, - AccessToken, - ProtectedResource, - ClientAuthentication - } -} \ No newline at end of file diff --git a/RestSharp/Authenticators/OAuth/OAuthWebQueryInfo.cs b/RestSharp/Authenticators/OAuth/OAuthWebQueryInfo.cs deleted file mode 100644 index f1156f4ae..000000000 --- a/RestSharp/Authenticators/OAuth/OAuthWebQueryInfo.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; - -namespace RestSharp.Authenticators.OAuth -{ -#if !SILVERLIGHT && !WINDOWS_PHONE && !PocketPC - [Serializable] -#endif - public class OAuthWebQueryInfo - { - public virtual string ConsumerKey { get; set; } - public virtual string Token { get; set; } - public virtual string Nonce { get; set; } - public virtual string Timestamp { get; set; } - public virtual string SignatureMethod { get; set; } - public virtual string Signature { get; set; } - public virtual string Version { get; set; } - public virtual string Callback { get; set; } - public virtual string Verifier { get; set; } - public virtual string ClientMode { get; set; } - public virtual string ClientUsername { get; set; } - public virtual string ClientPassword { get; set; } - public virtual string UserAgent { get; set; } - public virtual string WebMethod { get; set; } - public virtual OAuthParameterHandling ParameterHandling { get; set; } - public virtual OAuthSignatureTreatment SignatureTreatment { get; set; } - internal virtual string ConsumerSecret { get; set; } - internal virtual string TokenSecret { get; set; } - } -} \ No newline at end of file diff --git a/RestSharp/Authenticators/OAuth/OAuthWorkflow.cs b/RestSharp/Authenticators/OAuth/OAuthWorkflow.cs deleted file mode 100644 index 183974b9f..000000000 --- a/RestSharp/Authenticators/OAuth/OAuthWorkflow.cs +++ /dev/null @@ -1,408 +0,0 @@ -using System; -using System.Collections.Generic; -using RestSharp.Authenticators.OAuth.Extensions; -#if !WINDOWS_PHONE && !SILVERLIGHT && !PocketPC -using RestSharp.Contrib; -#endif - -namespace RestSharp.Authenticators.OAuth -{ - /// - /// A class to encapsulate OAuth authentication flow. - /// - /// - internal class OAuthWorkflow - { - public virtual string Version { get; set; } - public virtual string ConsumerKey { get; set; } - public virtual string ConsumerSecret { get; set; } - public virtual string Token { get; set; } - public virtual string TokenSecret { get; set; } - public virtual string CallbackUrl { get; set; } - public virtual string Verifier { get; set; } - public virtual string SessionHandle { get; set; } - - public virtual OAuthSignatureMethod SignatureMethod { get; set; } - public virtual OAuthSignatureTreatment SignatureTreatment { get; set; } - public virtual OAuthParameterHandling ParameterHandling { get; set; } - - public virtual string ClientUsername { get; set; } - public virtual string ClientPassword { get; set; } - - /// - public virtual string RequestTokenUrl { get; set; } - - /// - public virtual string AccessTokenUrl { get; set; } - - /// - public virtual string AuthorizationUrl { get; set; } - - /// - /// Generates a instance to pass to an - /// for the purpose of requesting an - /// unauthorized request token. - /// - /// The HTTP method for the intended request - /// - /// - public OAuthWebQueryInfo BuildRequestTokenInfo(string method) - { - return BuildRequestTokenInfo(method, null); - } - - /// - /// Generates a instance to pass to an - /// for the purpose of requesting an - /// unauthorized request token. - /// - /// The HTTP method for the intended request - /// Any existing, non-OAuth query parameters desired in the request - /// - /// - public virtual OAuthWebQueryInfo BuildRequestTokenInfo(string method, WebParameterCollection parameters) - { - ValidateTokenRequestState(); - - if (parameters == null) - { - parameters = new WebParameterCollection(); - } - - var timestamp = OAuthTools.GetTimestamp(); - var nonce = OAuthTools.GetNonce(); - - AddAuthParameters(parameters, timestamp, nonce); - - var signatureBase = OAuthTools.ConcatenateRequestElements(method, RequestTokenUrl, parameters); - var signature = OAuthTools.GetSignature(SignatureMethod, SignatureTreatment, signatureBase, ConsumerSecret); - - var info = new OAuthWebQueryInfo - { - WebMethod = method, - ParameterHandling = ParameterHandling, - ConsumerKey = ConsumerKey, - SignatureMethod = SignatureMethod.ToRequestValue(), - SignatureTreatment = SignatureTreatment, - Signature = signature, - Timestamp = timestamp, - Nonce = nonce, - Version = Version ?? "1.0", - Callback = OAuthTools.UrlEncodeRelaxed(CallbackUrl ?? ""), - TokenSecret = TokenSecret, - ConsumerSecret = ConsumerSecret - }; - - return info; - } - - /// - /// Generates a instance to pass to an - /// for the purpose of exchanging a request token - /// for an access token authorized by the user at the Service Provider site. - /// - /// The HTTP method for the intended request - /// - public virtual OAuthWebQueryInfo BuildAccessTokenInfo(string method) - { - return BuildAccessTokenInfo(method, null); - } - - /// - /// Generates a instance to pass to an - /// for the purpose of exchanging a request token - /// for an access token authorized by the user at the Service Provider site. - /// - /// The HTTP method for the intended request - /// - /// Any existing, non-OAuth query parameters desired in the request - public virtual OAuthWebQueryInfo BuildAccessTokenInfo(string method, WebParameterCollection parameters) - { - ValidateAccessRequestState(); - - if (parameters == null) - { - parameters = new WebParameterCollection(); - } - - var uri = new Uri(AccessTokenUrl); - var timestamp = OAuthTools.GetTimestamp(); - var nonce = OAuthTools.GetNonce(); - - AddAuthParameters(parameters, timestamp, nonce); - - var signatureBase = OAuthTools.ConcatenateRequestElements(method, uri.ToString(), parameters); - var signature = OAuthTools.GetSignature(SignatureMethod, SignatureTreatment, signatureBase, ConsumerSecret, TokenSecret); - - var info = new OAuthWebQueryInfo - { - WebMethod = method, - ParameterHandling = ParameterHandling, - ConsumerKey = ConsumerKey, - Token = Token, - SignatureMethod = SignatureMethod.ToRequestValue(), - SignatureTreatment = SignatureTreatment, - Signature = signature, - Timestamp = timestamp, - Nonce = nonce, - Version = Version ?? "1.0", - Verifier = Verifier, - Callback = CallbackUrl, - TokenSecret = TokenSecret, - ConsumerSecret = ConsumerSecret, - }; - - return info; - } - - /// - /// Generates a instance to pass to an - /// for the purpose of exchanging user credentials - /// for an access token authorized by the user at the Service Provider site. - /// - /// The HTTP method for the intended request - /// - /// Any existing, non-OAuth query parameters desired in the request - public virtual OAuthWebQueryInfo BuildClientAuthAccessTokenInfo(string method, WebParameterCollection parameters) - { - ValidateClientAuthAccessRequestState(); - - if (parameters == null) - { - parameters = new WebParameterCollection(); - } - - var uri = new Uri(AccessTokenUrl); - var timestamp = OAuthTools.GetTimestamp(); - var nonce = OAuthTools.GetNonce(); - - AddXAuthParameters(parameters, timestamp, nonce); - - var signatureBase = OAuthTools.ConcatenateRequestElements(method, uri.ToString(), parameters); - var signature = OAuthTools.GetSignature(SignatureMethod, SignatureTreatment, signatureBase, ConsumerSecret); - - var info = new OAuthWebQueryInfo - { - WebMethod = method, - ParameterHandling = ParameterHandling, - ClientMode = "client_auth", - ClientUsername = ClientUsername, - ClientPassword = ClientPassword, - ConsumerKey = ConsumerKey, - SignatureMethod = SignatureMethod.ToRequestValue(), - SignatureTreatment = SignatureTreatment, - Signature = signature, - Timestamp = timestamp, - Nonce = nonce, - Version = Version ?? "1.0", - TokenSecret = TokenSecret, - ConsumerSecret = ConsumerSecret - }; - - return info; - } - - public virtual OAuthWebQueryInfo BuildProtectedResourceInfo(string method, WebParameterCollection parameters, string url) - { - ValidateProtectedResourceState(); - - if (parameters == null) - { - parameters = new WebParameterCollection(); - } - - // Include url parameters in query pool - var uri = new Uri(url); -#if !SILVERLIGHT && !WINDOWS_PHONE && !PocketPC - var urlParameters = HttpUtility.ParseQueryString(uri.Query); -#else - var urlParameters = uri.Query.ParseQueryString(); -#endif - -#if !SILVERLIGHT && !WINDOWS_PHONE && !PocketPC - foreach (var parameter in urlParameters.AllKeys) -#else - foreach (var parameter in urlParameters.Keys) -#endif - { -#if PocketPC - switch (method.ToUpper()) -#else - switch (method.ToUpperInvariant()) -#endif - { - case "POST": - parameters.Add(new HttpPostParameter(parameter, urlParameters[parameter])); - break; - default: - parameters.Add(parameter, urlParameters[parameter]); - break; - } - } - - var timestamp = OAuthTools.GetTimestamp(); - var nonce = OAuthTools.GetNonce(); - - AddAuthParameters(parameters, timestamp, nonce); - - var signatureBase = OAuthTools.ConcatenateRequestElements(method, url, parameters); - - var signature = OAuthTools.GetSignature( - SignatureMethod, SignatureTreatment, signatureBase, ConsumerSecret, TokenSecret - ); - - var info = new OAuthWebQueryInfo - { - WebMethod = method, - ParameterHandling = ParameterHandling, - ConsumerKey = ConsumerKey, - Token = Token, - SignatureMethod = SignatureMethod.ToRequestValue(), - SignatureTreatment = SignatureTreatment, - Signature = signature, - Timestamp = timestamp, - Nonce = nonce, - Version = Version ?? "1.0", - Callback = CallbackUrl, - ConsumerSecret = ConsumerSecret, - TokenSecret = TokenSecret - }; - - return info; - } - - private void ValidateTokenRequestState() - { - if (RequestTokenUrl.IsNullOrBlank()) - { - throw new ArgumentException("You must specify a request token URL"); - } - - if (ConsumerKey.IsNullOrBlank()) - { - throw new ArgumentException("You must specify a consumer key"); - } - - if (ConsumerSecret.IsNullOrBlank()) - { - throw new ArgumentException("You must specify a consumer secret"); - } - } - - private void ValidateAccessRequestState() - { - if (AccessTokenUrl.IsNullOrBlank()) - { - throw new ArgumentException("You must specify an access token URL"); - } - - if (ConsumerKey.IsNullOrBlank()) - { - throw new ArgumentException("You must specify a consumer key"); - } - - if (ConsumerSecret.IsNullOrBlank()) - { - throw new ArgumentException("You must specify a consumer secret"); - } - - if (Token.IsNullOrBlank()) - { - throw new ArgumentException("You must specify a token"); - } - } - - private void ValidateClientAuthAccessRequestState() - { - if (AccessTokenUrl.IsNullOrBlank()) - { - throw new ArgumentException("You must specify an access token URL"); - } - - if (ConsumerKey.IsNullOrBlank()) - { - throw new ArgumentException("You must specify a consumer key"); - } - - if (ConsumerSecret.IsNullOrBlank()) - { - throw new ArgumentException("You must specify a consumer secret"); - } - - if (ClientUsername.IsNullOrBlank() || ClientPassword.IsNullOrBlank()) - { - throw new ArgumentException("You must specify user credentials"); - } - } - - private void ValidateProtectedResourceState() - { - if (ConsumerKey.IsNullOrBlank()) - { - throw new ArgumentException("You must specify a consumer key"); - } - - if (ConsumerSecret.IsNullOrBlank()) - { - throw new ArgumentException("You must specify a consumer secret"); - } - } - - private void AddAuthParameters(ICollection parameters, string timestamp, string nonce) - { - var authParameters = new WebParameterCollection - { - new WebPair("oauth_consumer_key", ConsumerKey), - new WebPair("oauth_nonce", nonce), - new WebPair("oauth_signature_method", SignatureMethod.ToRequestValue()), - new WebPair("oauth_timestamp", timestamp), - new WebPair("oauth_version", Version ?? "1.0") - }; - - if (!Token.IsNullOrBlank()) - { - authParameters.Add(new WebPair("oauth_token", Token)); - } - - if (!CallbackUrl.IsNullOrBlank()) - { - authParameters.Add(new WebPair("oauth_callback", CallbackUrl)); - } - - if (!Verifier.IsNullOrBlank()) - { - authParameters.Add(new WebPair("oauth_verifier", Verifier)); - } - - if (!SessionHandle.IsNullOrBlank()) - { - authParameters.Add(new WebPair("oauth_session_handle", SessionHandle)); - } - - foreach (var authParameter in authParameters) - { - parameters.Add(authParameter); - } - } - - private void AddXAuthParameters(ICollection parameters, string timestamp, string nonce) - { - var authParameters = new WebParameterCollection - { - new WebPair("x_auth_username", ClientUsername), - new WebPair("x_auth_password", ClientPassword), - new WebPair("x_auth_mode", "client_auth"), - new WebPair("oauth_consumer_key", ConsumerKey), - new WebPair("oauth_signature_method", SignatureMethod.ToRequestValue()), - new WebPair("oauth_timestamp", timestamp), - new WebPair("oauth_nonce", nonce), - new WebPair("oauth_version", Version ?? "1.0") - }; - - foreach (var authParameter in authParameters) - { - parameters.Add(authParameter); - } - } - } -} \ No newline at end of file diff --git a/RestSharp/Authenticators/OAuth/WebPair.cs b/RestSharp/Authenticators/OAuth/WebPair.cs deleted file mode 100644 index 4ecc9c42b..000000000 --- a/RestSharp/Authenticators/OAuth/WebPair.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace RestSharp.Authenticators.OAuth -{ - internal class WebPair - { - public WebPair(string name, string value) - { - Name = name; - Value = value; - } - - public string Value { get; set; } - public string Name { get; set; } - } -} \ No newline at end of file diff --git a/RestSharp/Authenticators/OAuth/WebPairCollection.cs b/RestSharp/Authenticators/OAuth/WebPairCollection.cs deleted file mode 100644 index db9c7157b..000000000 --- a/RestSharp/Authenticators/OAuth/WebPairCollection.cs +++ /dev/null @@ -1,192 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Linq; - -namespace RestSharp.Authenticators.OAuth -{ - internal class WebPairCollection : IList - { - private IList _parameters; - - public virtual WebPair this[string name] - { - get { return this.SingleOrDefault(p => p.Name.Equals(name)); } - } - - public virtual IEnumerable Names - { - get { return _parameters.Select(p => p.Name); } - } - - public virtual IEnumerable Values - { - get { return _parameters.Select(p => p.Value); } - } - - public WebPairCollection(IEnumerable parameters) - { - _parameters = new List(parameters); - } - -#if !WINDOWS_PHONE && !SILVERLIGHT && !PocketPC - public WebPairCollection(NameValueCollection collection) : this() - { - AddCollection(collection); - } - - public virtual void AddRange(NameValueCollection collection) - { - AddCollection(collection); - } - - private void AddCollection(NameValueCollection collection) - { - var parameters = collection.AllKeys.Select(key => new WebPair(key, collection[key])); - foreach (var parameter in parameters) - { - _parameters.Add(parameter); - } - } -#endif - - public WebPairCollection(IDictionary collection) : this() - { - AddCollection(collection); - } - - public void AddCollection(IDictionary collection) - { - foreach (var key in collection.Keys) - { - var parameter = new WebPair(key, collection[key]); - _parameters.Add(parameter); - } - } - - public WebPairCollection() - { - _parameters = new List(0); - } - - public WebPairCollection(int capacity) - { - _parameters = new List(capacity); - } - - private void AddCollection(IEnumerable collection) - { - foreach (var parameter in collection) - { - var pair = new WebPair(parameter.Name, parameter.Value); - _parameters.Add(pair); - } - } - - public virtual void AddRange(WebPairCollection collection) - { - AddCollection(collection); - } - - public virtual void AddRange(IEnumerable collection) - { - AddCollection(collection); - } - - public virtual void Sort(Comparison comparison) - { - var sorted = new List(_parameters); - sorted.Sort(comparison); - _parameters = sorted; - } - - public virtual bool RemoveAll(IEnumerable parameters) - { - var success = true; - var array = parameters.ToArray(); - for (var p = 0; p < array.Length; p++) - { - var parameter = array[p]; - success &= _parameters.Remove(parameter); - } - return success && array.Length > 0; - } - - public virtual void Add(string name, string value) - { - var pair = new WebPair(name, value); - _parameters.Add(pair); - } - - #region IList Members - - public virtual IEnumerator GetEnumerator() - { - return _parameters.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - public virtual void Add(WebPair parameter) - { - _parameters.Add(parameter); - } - - public virtual void Clear() - { - _parameters.Clear(); - } - - public virtual bool Contains(WebPair parameter) - { - return _parameters.Contains(parameter); - } - - public virtual void CopyTo(WebPair[] parameters, int arrayIndex) - { - _parameters.CopyTo(parameters, arrayIndex); - } - - public virtual bool Remove(WebPair parameter) - { - return _parameters.Remove(parameter); - } - - public virtual int Count - { - get { return _parameters.Count; } - } - - public virtual bool IsReadOnly - { - get { return _parameters.IsReadOnly; } - } - - public virtual int IndexOf(WebPair parameter) - { - return _parameters.IndexOf(parameter); - } - - public virtual void Insert(int index, WebPair parameter) - { - _parameters.Insert(index, parameter); - } - - public virtual void RemoveAt(int index) - { - _parameters.RemoveAt(index); - } - - public virtual WebPair this[int index] - { - get { return _parameters[index]; } - set { _parameters[index] = value; } - } - - #endregion - } -} \ No newline at end of file diff --git a/RestSharp/Authenticators/OAuth/WebParameter.cs b/RestSharp/Authenticators/OAuth/WebParameter.cs deleted file mode 100644 index 5f46c794e..000000000 --- a/RestSharp/Authenticators/OAuth/WebParameter.cs +++ /dev/null @@ -1,21 +0,0 @@ -#if !Smartphone -using System; -using System.Diagnostics; - -#endif - -namespace RestSharp.Authenticators.OAuth -{ -#if !Smartphone && !PocketPC - [DebuggerDisplay("{Name}:{Value}")] -#endif -#if !SILVERLIGHT && !WINDOWS_PHONE && !PocketPC - [Serializable] -#endif - internal class WebParameter : WebPair - { - public WebParameter(string name, string value) : base(name, value) - { - } - } -} \ No newline at end of file diff --git a/RestSharp/Authenticators/OAuth/WebParameterCollection.cs b/RestSharp/Authenticators/OAuth/WebParameterCollection.cs deleted file mode 100644 index 1da58f01d..000000000 --- a/RestSharp/Authenticators/OAuth/WebParameterCollection.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Collections.Generic; -using System.Collections.Specialized; - -namespace RestSharp.Authenticators.OAuth -{ - internal class WebParameterCollection : WebPairCollection - { - public WebParameterCollection(IEnumerable parameters) - : base(parameters) - { - } - -#if !WINDOWS_PHONE && !SILVERLIGHT && !PocketPC - public WebParameterCollection(NameValueCollection collection) : base(collection) - { - } -#endif - - public WebParameterCollection() - { - } - - public WebParameterCollection(int capacity) : base(capacity) - { - } - - public WebParameterCollection(IDictionary collection) : base(collection) - { - } - - public override void Add(string name, string value) - { - var parameter = new WebParameter(name, value); - base.Add(parameter); - } - } -} \ No newline at end of file diff --git a/RestSharp/Authenticators/OAuth1Authenticator.cs b/RestSharp/Authenticators/OAuth1Authenticator.cs deleted file mode 100644 index bdde9c08c..000000000 --- a/RestSharp/Authenticators/OAuth1Authenticator.cs +++ /dev/null @@ -1,263 +0,0 @@ -using System; -using System.Linq; -using System.Text; -using RestSharp.Authenticators.OAuth; -using RestSharp.Authenticators.OAuth.Extensions; - -#if WINDOWS_PHONE -using System.Net; -#elif SILVERLIGHT -using System.Windows.Browser; -#else -using RestSharp.Contrib; -#endif - - -namespace RestSharp.Authenticators -{ - /// - public class OAuth1Authenticator : IAuthenticator - { - public virtual string Realm { get; set; } - public virtual OAuthParameterHandling ParameterHandling { get; set; } - public virtual OAuthSignatureMethod SignatureMethod { get; set; } - public virtual OAuthSignatureTreatment SignatureTreatment { get; set; } - - internal virtual OAuthType Type { get; set; } - internal virtual string ConsumerKey { get; set; } - internal virtual string ConsumerSecret { get; set; } - internal virtual string Token { get; set; } - internal virtual string TokenSecret { get; set; } - internal virtual string Verifier { get; set; } - internal virtual string Version { get; set; } - internal virtual string CallbackUrl { get; set; } - internal virtual string SessionHandle { get; set; } - internal virtual string ClientUsername { get; set; } - internal virtual string ClientPassword { get; set; } - - public static OAuth1Authenticator ForRequestToken(string consumerKey, string consumerSecret) - { - var authenticator = new OAuth1Authenticator - { - ParameterHandling = OAuthParameterHandling.HttpAuthorizationHeader, - SignatureMethod = OAuthSignatureMethod.HmacSha1, - SignatureTreatment = OAuthSignatureTreatment.Escaped, - ConsumerKey = consumerKey, - ConsumerSecret = consumerSecret, - Type = OAuthType.RequestToken - }; - return authenticator; - } - - public static OAuth1Authenticator ForRequestToken(string consumerKey, string consumerSecret, string callbackUrl) - { - var authenticator = ForRequestToken(consumerKey, consumerSecret); - authenticator.CallbackUrl = callbackUrl; - return authenticator; - } - - public static OAuth1Authenticator ForAccessToken(string consumerKey, string consumerSecret, string token, string tokenSecret) - { - var authenticator = new OAuth1Authenticator - { - ParameterHandling = OAuthParameterHandling.HttpAuthorizationHeader, - SignatureMethod = OAuthSignatureMethod.HmacSha1, - SignatureTreatment = OAuthSignatureTreatment.Escaped, - ConsumerKey = consumerKey, - ConsumerSecret = consumerSecret, - Token = token, - TokenSecret = tokenSecret, - Type = OAuthType.AccessToken - }; - return authenticator; - } - - public static OAuth1Authenticator ForAccessToken(string consumerKey, string consumerSecret, string token, string tokenSecret, string verifier) - { - var authenticator = ForAccessToken(consumerKey, consumerSecret, token, tokenSecret); - authenticator.Verifier = verifier; - return authenticator; - } - - public static OAuth1Authenticator ForAccessTokenRefresh(string consumerKey, string consumerSecret, string token, string tokenSecret, string sessionHandle) - { - var authenticator = ForAccessToken(consumerKey, consumerSecret, token, tokenSecret); - authenticator.SessionHandle = sessionHandle; - return authenticator; - } - - public static OAuth1Authenticator ForAccessTokenRefresh(string consumerKey, string consumerSecret, string token, string tokenSecret, string verifier, string sessionHandle) - { - var authenticator = ForAccessToken(consumerKey, consumerSecret, token, tokenSecret); - authenticator.SessionHandle = sessionHandle; - authenticator.Verifier = verifier; - return authenticator; - } - - public static OAuth1Authenticator ForClientAuthentication(string consumerKey, string consumerSecret, string username, string password) - { - var authenticator = new OAuth1Authenticator - { - ParameterHandling = OAuthParameterHandling.HttpAuthorizationHeader, - SignatureMethod = OAuthSignatureMethod.HmacSha1, - SignatureTreatment = OAuthSignatureTreatment.Escaped, - ConsumerKey = consumerKey, - ConsumerSecret = consumerSecret, - ClientUsername = username, - ClientPassword = password, - Type = OAuthType.ClientAuthentication - }; - return authenticator; - } - - public static OAuth1Authenticator ForProtectedResource(string consumerKey, string consumerSecret, string accessToken, string accessTokenSecret) - { - var authenticator = new OAuth1Authenticator - { - Type = OAuthType.ProtectedResource, - ParameterHandling = OAuthParameterHandling.HttpAuthorizationHeader, - SignatureMethod = OAuthSignatureMethod.HmacSha1, - SignatureTreatment = OAuthSignatureTreatment.Escaped, - ConsumerKey = consumerKey, - ConsumerSecret = consumerSecret, - Token = accessToken, - TokenSecret = accessTokenSecret - }; - return authenticator; - } - - public void Authenticate(IRestClient client, IRestRequest request) - { - var workflow = new OAuthWorkflow - { - ConsumerKey = ConsumerKey, - ConsumerSecret = ConsumerSecret, - ParameterHandling = ParameterHandling, - SignatureMethod = SignatureMethod, - SignatureTreatment = SignatureTreatment, - Verifier = Verifier, - Version = Version, - CallbackUrl = CallbackUrl, - SessionHandle = SessionHandle, - Token = Token, - TokenSecret = TokenSecret, - ClientUsername = ClientUsername, - ClientPassword = ClientPassword - }; - - AddOAuthData(client, request, workflow); - } - - private void AddOAuthData(IRestClient client, IRestRequest request, OAuthWorkflow workflow) - { - var url = client.BuildUri(request).ToString(); - var queryStringStart = url.IndexOf('?'); - if (queryStringStart != -1) - url = url.Substring(0, queryStringStart); - - OAuthWebQueryInfo oauth; -#if PocketPC - var method = request.Method.ToString().ToUpper(); -#else - var method = request.Method.ToString().ToUpperInvariant(); -#endif - - var parameters = new WebParameterCollection(); - - // include all GET and POST parameters before generating the signature - // according to the RFC 5849 - The OAuth 1.0 Protocol - // http://tools.ietf.org/html/rfc5849#section-3.4.1 - // if this change causes trouble we need to introduce a flag indicating the specific OAuth implementation level, - // or implement a seperate class for each OAuth version - if (!request.AlwaysMultipartFormData && !request.Files.Any()) - { - foreach (var p in client.DefaultParameters.Where(p => p.Type == ParameterType.GetOrPost)) - { - parameters.Add(new WebPair(p.Name, p.Value.ToString())); - } - foreach (var p in request.Parameters.Where(p => p.Type == ParameterType.GetOrPost)) - { - parameters.Add(new WebPair(p.Name, p.Value.ToString())); - } - } - else - { - // if we are sending a multipart request, only the "oauth_" parameters should be included in the signature - foreach ( - var p in client.DefaultParameters.Where(p => p.Type == ParameterType.GetOrPost && p.Name.StartsWith("oauth_"))) - { - parameters.Add(new WebPair(p.Name, p.Value.ToString())); - } - foreach (var p in request.Parameters.Where(p => p.Type == ParameterType.GetOrPost && p.Name.StartsWith("oauth_"))) - { - parameters.Add(new WebPair(p.Name, p.Value.ToString())); - } - } - - switch (Type) - { - case OAuthType.RequestToken: - workflow.RequestTokenUrl = url; - oauth = workflow.BuildRequestTokenInfo(method, parameters); - break; - case OAuthType.AccessToken: - workflow.AccessTokenUrl = url; - oauth = workflow.BuildAccessTokenInfo(method, parameters); - break; - case OAuthType.ClientAuthentication: - workflow.AccessTokenUrl = url; - oauth = workflow.BuildClientAuthAccessTokenInfo(method, parameters); - break; - case OAuthType.ProtectedResource: - oauth = workflow.BuildProtectedResourceInfo(method, parameters, url); - break; - default: - throw new ArgumentOutOfRangeException(); - } - - switch (ParameterHandling) - { - case OAuthParameterHandling.HttpAuthorizationHeader: - parameters.Add("oauth_signature", oauth.Signature); - request.AddHeader("Authorization", GetAuthorizationHeader(parameters)); - break; - case OAuthParameterHandling.UrlOrPostParameters: - parameters.Add("oauth_signature", oauth.Signature); - foreach (var parameter in parameters.Where(parameter => !parameter.Name.IsNullOrBlank() && (parameter.Name.StartsWith("oauth_") || parameter.Name.StartsWith("x_auth_")))) - { - request.AddParameter(parameter.Name, HttpUtility.UrlDecode(parameter.Value)); - } - break; - default: - throw new ArgumentOutOfRangeException(); - } - } - - private string GetAuthorizationHeader(WebPairCollection parameters) - { - var sb = new StringBuilder("OAuth "); - if (!Realm.IsNullOrBlank()) - { - sb.Append("realm=\"{0}\",".FormatWith(OAuthTools.UrlEncodeRelaxed(Realm))); - } - - parameters.Sort((l, r) => l.Name.CompareTo(r.Name)); - - var parameterCount = 0; - var oathParameters = parameters.Where(parameter => - !parameter.Name.IsNullOrBlank() && - !parameter.Value.IsNullOrBlank() && - (parameter.Name.StartsWith("oauth_") || parameter.Name.StartsWith("x_auth_")) - ).ToList(); - foreach (var parameter in oathParameters) - { - parameterCount++; - var format = parameterCount < oathParameters.Count ? "{0}=\"{1}\"," : "{0}=\"{1}\""; - sb.Append(format.FormatWith(parameter.Name, parameter.Value)); - } - - var authorization = sb.ToString(); - return authorization; - } - } -} diff --git a/RestSharp/Authenticators/OAuth2Authenticator.cs b/RestSharp/Authenticators/OAuth2Authenticator.cs deleted file mode 100644 index 7fd457929..000000000 --- a/RestSharp/Authenticators/OAuth2Authenticator.cs +++ /dev/null @@ -1,120 +0,0 @@ -using System; -using System.Linq; - -namespace RestSharp -{ - /// - /// Base class for OAuth 2 Authenticators. - /// - /// - /// Since there are many ways to authenticate in OAuth2, - /// this is used as a base class to differentiate between - /// other authenticators. - /// - /// Any other OAuth2 authenticators must derive from this - /// abstract class. - /// - public abstract class OAuth2Authenticator : IAuthenticator - { - /// - /// Access token to be used when authenticating. - /// - private readonly string _accessToken; - - /// - /// Initializes a new instance of the class. - /// - /// - /// The access token. - /// - public OAuth2Authenticator(string accessToken) - { - _accessToken = accessToken; - } - - /// - /// Gets the access token. - /// - public string AccessToken - { - get { return _accessToken; } - } - - public abstract void Authenticate(IRestClient client, IRestRequest request); - } - - /// - /// The OAuth 2 authenticator using URI query parameter. - /// - /// - /// Based on http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-5.1.2 - /// - public class OAuth2UriQueryParameterAuthenticator : OAuth2Authenticator - { - /// - /// Initializes a new instance of the class. - /// - /// - /// The access token. - /// - public OAuth2UriQueryParameterAuthenticator(string accessToken) - : base(accessToken) - { - } - - public override void Authenticate(IRestClient client, IRestRequest request) - { - request.AddParameter("oauth_token", AccessToken, ParameterType.GetOrPost); - } - } - - /// - /// The OAuth 2 authenticator using the authorization request header field. - /// - /// - /// Based on http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-5.1.1 - /// - public class OAuth2AuthorizationRequestHeaderAuthenticator : OAuth2Authenticator - { - /// - /// Stores the Authorization header value as "[tokenType] accessToken". used for performance. - /// - private readonly string _authorizationValue; - - /// - /// Initializes a new instance of the class. - /// - /// - /// The access token. - /// - public OAuth2AuthorizationRequestHeaderAuthenticator(string accessToken) - : this(accessToken, "OAuth") - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// The access token. - /// - /// - /// The token type. - /// - public OAuth2AuthorizationRequestHeaderAuthenticator(string accessToken, string tokenType) - : base(accessToken) - { - // Conatenate during constructor so that it is only done once. can improve performance. - _authorizationValue = tokenType + " " + accessToken; - } - - public override void Authenticate(IRestClient client, IRestRequest request) - { - // only add the Authorization parameter if it hasn't been added. - if (!request.Parameters.Any(p => p.Name.Equals("Authorization", StringComparison.OrdinalIgnoreCase))) - { - request.AddParameter("Authorization", _authorizationValue, ParameterType.HttpHeader); - } - } - } -} \ No newline at end of file diff --git a/RestSharp/Authenticators/SimpleAuthenticator.cs b/RestSharp/Authenticators/SimpleAuthenticator.cs deleted file mode 100644 index 1a89399e2..000000000 --- a/RestSharp/Authenticators/SimpleAuthenticator.cs +++ /dev/null @@ -1,39 +0,0 @@ -#region License -// Copyright 2010 John Sheehan -// -// 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. -#endregion - -namespace RestSharp -{ - public class SimpleAuthenticator : IAuthenticator - { - private readonly string _usernameKey; - private readonly string _username; - private readonly string _passwordKey; - private readonly string _password; - - public SimpleAuthenticator(string usernameKey, string username, string passwordKey, string password) { - _usernameKey = usernameKey; - _username = username; - _passwordKey = passwordKey; - _password = password; - } - - public void Authenticate(IRestClient client, IRestRequest request) - { - request.AddParameter(_usernameKey, _username); - request.AddParameter(_passwordKey, _password); - } - } -} diff --git a/RestSharp/Compression/ZLib/Crc32.cs b/RestSharp/Compression/ZLib/Crc32.cs deleted file mode 100644 index 0dd8d7009..000000000 --- a/RestSharp/Compression/ZLib/Crc32.cs +++ /dev/null @@ -1,469 +0,0 @@ -// Crc32.cs -// ------------------------------------------------------------------ -// -// Copyright (c) 2006-2009 Dino Chiesa and Microsoft Corporation. -// All rights reserved. -// -// This code module is part of DotNetZip, a zipfile class library. -// -// ------------------------------------------------------------------ -// -// This code is licensed under the Microsoft Public License. -// See the file License.txt for the license details. -// More info on: http://dotnetzip.codeplex.com -// -// ------------------------------------------------------------------ -// -// last saved (in emacs): -// Time-stamp: <2009-August-18 21:04:21> -// -// ------------------------------------------------------------------ -// -// Implements the CRC algorithm, which is used in zip files. The zip format calls for -// the zipfile to contain a CRC for the unencrypted byte stream of each file. -// -// It is based on example source code published at -// http://www.vbaccelerator.com/home/net/code/libraries/CRC32/Crc32_zip_CRC32_CRC32_cs.asp -// -// This implementation adds a tweak of that code for use within zip creation. While -// computing the CRC we also compress the byte stream, in the same read loop. This -// avoids the need to read through the uncompressed stream twice - once to compute CRC -// and another time to compress. -// -// ------------------------------------------------------------------ - -#if WINDOWS_PHONE - -using System; -using Interop = System.Runtime.InteropServices; - - -namespace RestSharp.Compression.ZLib -{ - /// - /// Calculates a 32bit Cyclic Redundancy Checksum (CRC) using the same polynomial - /// used by Zip. This type is used internally by DotNetZip; it is generally not used - /// directly by applications wishing to create, read, or manipulate zip archive - /// files. - /// - - internal class CRC32 - { - /// - /// indicates the total number of bytes read on the CRC stream. - /// This is used when writing the ZipDirEntry when compressing files. - /// - public Int64 TotalBytesRead - { - get - { - return _TotalBytesRead; - } - } - - /// - /// Indicates the current CRC for all blocks slurped in. - /// - public Int32 Crc32Result - { - get - { - // return one's complement of the running result - return unchecked((Int32)(~_RunningCrc32Result)); - } - } - - /// - /// Returns the CRC32 for the specified stream. - /// - /// The stream over which to calculate the CRC32 - /// the CRC32 calculation - public Int32 GetCrc32(System.IO.Stream input) - { - return GetCrc32AndCopy(input, null); - } - - /// - /// Returns the CRC32 for the specified stream, and writes the input into the - /// output stream. - /// - /// The stream over which to calculate the CRC32 - /// The stream into which to deflate the input - /// the CRC32 calculation - public Int32 GetCrc32AndCopy(System.IO.Stream input, System.IO.Stream output) - { - if (input == null) - throw new ZlibException("The input stream must not be null."); - - unchecked - { - //UInt32 crc32Result; - //crc32Result = 0xFFFFFFFF; - byte[] buffer = new byte[BUFFER_SIZE]; - int readSize = BUFFER_SIZE; - - _TotalBytesRead = 0; - int count = input.Read(buffer, 0, readSize); - if (output != null) output.Write(buffer, 0, count); - _TotalBytesRead += count; - while (count > 0) - { - SlurpBlock(buffer, 0, count); - count = input.Read(buffer, 0, readSize); - if (output != null) output.Write(buffer, 0, count); - _TotalBytesRead += count; - } - - return (Int32)(~_RunningCrc32Result); - } - } - - - /// - /// Get the CRC32 for the given (word,byte) combo. This is a computation - /// defined by PKzip. - /// - /// The word to start with. - /// The byte to combine it with. - /// The CRC-ized result. - public Int32 ComputeCrc32(Int32 W, byte B) - { - return _InternalComputeCrc32((UInt32)W, B); - } - - internal Int32 _InternalComputeCrc32(UInt32 W, byte B) - { - return (Int32)(crc32Table[(W ^ B) & 0xFF] ^ (W >> 8)); - } - - /// - /// Update the value for the running CRC32 using the given block of bytes. - /// This is useful when using the CRC32() class in a Stream. - /// - /// block of bytes to slurp - /// starting point in the block - /// how many bytes within the block to slurp - public void SlurpBlock(byte[] block, int offset, int count) - { - if (block == null) - throw new ZlibException("The data buffer must not be null."); - - for (int i = 0; i < count; i++) - { - int x = offset + i; - _RunningCrc32Result = ((_RunningCrc32Result) >> 8) ^ crc32Table[(block[x]) ^ ((_RunningCrc32Result) & 0x000000FF)]; - } - _TotalBytesRead += count; - } - - - // pre-initialize the crc table for speed of lookup. - static CRC32() - { - unchecked - { - // This is the official polynomial used by CRC32 in PKZip. - // Often the polynomial is shown reversed as 0x04C11DB7. - UInt32 dwPolynomial = 0xEDB88320; - UInt32 i, j; - - crc32Table = new UInt32[256]; - - UInt32 dwCrc; - for (i = 0; i < 256; i++) - { - dwCrc = i; - for (j = 8; j > 0; j--) - { - if ((dwCrc & 1) == 1) - { - dwCrc = (dwCrc >> 1) ^ dwPolynomial; - } - else - { - dwCrc >>= 1; - } - } - crc32Table[i] = dwCrc; - } - } - } - - - // private member vars - private Int64 _TotalBytesRead; - private static UInt32[] crc32Table; - private const int BUFFER_SIZE = 8192; - private UInt32 _RunningCrc32Result = 0xFFFFFFFF; - - } - - /// - /// A Stream that calculates a CRC32 (a checksum) on all bytes read, - /// or on all bytes written. - /// - /// - /// - /// - /// This class can be used to verify the CRC of a ZipEntry when - /// reading from a stream, or to calculate a CRC when writing to a - /// stream. The stream should be used to either read, or write, but - /// not both. If you intermix reads and writes, the results are not - /// defined. - /// - /// - /// - /// This class is intended primarily for use internally by the - /// DotNetZip library. - /// - /// - internal class CrcCalculatorStream : System.IO.Stream, System.IDisposable - { - private static readonly Int64 UnsetLengthLimit = -99; - - private System.IO.Stream _innerStream; - private CRC32 _Crc32; - private Int64 _lengthLimit = -99; - private bool _leaveOpen; - - /// - /// Gets the total number of bytes run through the CRC32 calculator. - /// - /// - /// - /// This is either the total number of bytes read, or the total number of bytes - /// written, depending on the direction of this stream. - /// - public Int64 TotalBytesSlurped - { - get { return _Crc32.TotalBytesRead; } - } - - - /// - /// The default constructor. - /// - /// - /// Instances returned from this constructor will leave the underlying stream - /// open upon Close(). - /// - /// The underlying stream - public CrcCalculatorStream(System.IO.Stream stream) - : this(true, CrcCalculatorStream.UnsetLengthLimit, stream) - { - } - - - /// - /// The constructor allows the caller to specify how to handle the underlying - /// stream at close. - /// - /// The underlying stream - /// true to leave the underlying stream - /// open upon close of the CrcCalculatorStream.; false otherwise. - public CrcCalculatorStream(System.IO.Stream stream, bool leaveOpen) - : this(leaveOpen, CrcCalculatorStream.UnsetLengthLimit, stream) - { - } - - - /// - /// A constructor allowing the specification of the length of the stream to read. - /// - /// - /// Instances returned from this constructor will leave the underlying stream open - /// upon Close(). - /// - /// The underlying stream - /// The length of the stream to slurp - public CrcCalculatorStream(System.IO.Stream stream, Int64 length) - : this(true, length, stream) - { - if (length < 0) - throw new ArgumentException("length"); - } - - /// - /// A constructor allowing the specification of the length of the stream to - /// read, as well as whether to keep the underlying stream open upon Close(). - /// - /// The underlying stream - /// The length of the stream to slurp - /// true to leave the underlying stream - /// open upon close of the CrcCalculatorStream.; false otherwise. - public CrcCalculatorStream(System.IO.Stream stream, Int64 length, bool leaveOpen) - : this(leaveOpen, length, stream) - { - if (length < 0) - throw new ArgumentException("length"); - } - - - // This ctor is private - no validation is done here. This is to allow the use - // of a (specific) negative value for the _lengthLimit, to indicate that there - // is no length set. So we validate the length limit in those ctors that use an - // explicit param, otherwise we don't validate, because it could be our special - // value. - private CrcCalculatorStream(bool leaveOpen, Int64 length, System.IO.Stream stream) - : base() - { - _innerStream = stream; - _Crc32 = new CRC32(); - _lengthLimit = length; - _leaveOpen = leaveOpen; - } - - /// - /// Provides the current CRC for all blocks slurped in. - /// - public Int32 Crc - { - get { return _Crc32.Crc32Result; } - } - - /// - /// Indicates whether the underlying stream will be left open when the - /// CrcCalculatorStream is Closed. - /// - public bool LeaveOpen - { - get { return _leaveOpen; } - set { _leaveOpen = value; } - } - - /// - /// Read from the stream - /// - /// the buffer to read - /// the offset at which to start - /// the number of bytes to read - /// the number of bytes actually read - public override int Read(byte[] buffer, int offset, int count) - { - int bytesToRead = count; - - // Need to limit the # of bytes returned, if the stream is intended to have - // a definite length. This is especially useful when returning a stream for - // the uncompressed data directly to the application. The app won't - // necessarily read only the UncompressedSize number of bytes. For example - // wrapping the stream returned from OpenReader() into a StreadReader() and - // calling ReadToEnd() on it, We can "over-read" the zip data and get a - // corrupt string. The length limits that, prevents that problem. - - if (_lengthLimit != CrcCalculatorStream.UnsetLengthLimit) - { - if (_Crc32.TotalBytesRead >= _lengthLimit) return 0; // EOF - Int64 bytesRemaining = _lengthLimit - _Crc32.TotalBytesRead; - if (bytesRemaining < count) bytesToRead = (int)bytesRemaining; - } - int n = _innerStream.Read(buffer, offset, bytesToRead); - if (n > 0) _Crc32.SlurpBlock(buffer, offset, n); - return n; - } - - /// - /// Write to the stream. - /// - /// the buffer from which to write - /// the offset at which to start writing - /// the number of bytes to write - public override void Write(byte[] buffer, int offset, int count) - { - if (count > 0) _Crc32.SlurpBlock(buffer, offset, count); - _innerStream.Write(buffer, offset, count); - } - - /// - /// Indicates whether the stream supports reading. - /// - public override bool CanRead - { - get { return _innerStream.CanRead; } - } - - /// - /// Indicates whether the stream supports seeking. - /// - public override bool CanSeek - { - get { return _innerStream.CanSeek; } - } - - /// - /// Indicates whether the stream supports writing. - /// - public override bool CanWrite - { - get { return _innerStream.CanWrite; } - } - - /// - /// Flush the stream. - /// - public override void Flush() - { - _innerStream.Flush(); - } - - /// - /// Not implemented. - /// - public override long Length - { - get - { - if (_lengthLimit == CrcCalculatorStream.UnsetLengthLimit) - return _innerStream.Length; - else return _lengthLimit; - } - } - - /// - /// Not implemented. - /// - public override long Position - { - get { return _Crc32.TotalBytesRead; } - set { throw new NotImplementedException(); } - } - - /// - /// Not implemented. - /// - /// N/A - /// N/A - /// N/A - public override long Seek(long offset, System.IO.SeekOrigin origin) - { - throw new NotImplementedException(); - } - - /// - /// Not implemented. - /// - /// N/A - public override void SetLength(long value) - { - throw new NotImplementedException(); - } - - - void IDisposable.Dispose() - { - Close(); - } - - /// - /// Closes the stream. - /// - public override void Close() - { - base.Close(); - if (!_leaveOpen) - _innerStream.Close(); - } - - } -} - -#endif \ No newline at end of file diff --git a/RestSharp/Compression/ZLib/FlushType.cs b/RestSharp/Compression/ZLib/FlushType.cs deleted file mode 100644 index 201152a9a..000000000 --- a/RestSharp/Compression/ZLib/FlushType.cs +++ /dev/null @@ -1,52 +0,0 @@ -#if WINDOWS_PHONE - -using System; -using System.Net; - -namespace RestSharp.Compression.ZLib -{ - - /// - /// Describes how to flush the current deflate operation. - /// - /// - /// The different FlushType values are useful when using a Deflate in a streaming application. - /// - internal enum FlushType - { - /// No flush at all. - None = 0, - - /// Closes the current block, but doesn't flush it to - /// the output. Used internally only in hypothetical - /// scenarios. This was supposed to be removed by Zlib, but it is - /// still in use in some edge cases. - /// - Partial, - - /// - /// Use this during compression to specify that all pending output should be - /// flushed to the output buffer and the output should be aligned on a byte - /// boundary. You might use this in a streaming communication scenario, so that - /// the decompressor can get all input data available so far. When using this - /// with a ZlibCodec, AvailableBytesIn will be zero after the call if - /// enough output space has been provided before the call. Flushing will - /// degrade compression and so it should be used only when necessary. - /// - Sync, - - /// - /// Use this during compression to specify that all output should be flushed, as - /// with FlushType.Sync, but also, the compression state should be reset - /// so that decompression can restart from this point if previous compressed - /// data has been damaged or if random access is desired. Using - /// FlushType.Full too often can significantly degrade the compression. - /// - Full, - - /// Signals the end of the compression/decompression stream. - Finish, - } -} - -#endif \ No newline at end of file diff --git a/RestSharp/Compression/ZLib/GZipStream.cs b/RestSharp/Compression/ZLib/GZipStream.cs deleted file mode 100644 index cd43c1f4e..000000000 --- a/RestSharp/Compression/ZLib/GZipStream.cs +++ /dev/null @@ -1,598 +0,0 @@ -// GZipStream.cs -// ------------------------------------------------------------------ -// -// Copyright (c) 2009 Dino Chiesa and Microsoft Corporation. -// All rights reserved. -// -// This code module is part of DotNetZip, a zipfile class library. -// -// ------------------------------------------------------------------ -// -// This code is licensed under the Microsoft Public License. -// See the file License.txt for the license details. -// More info on: http://dotnetzip.codeplex.com -// -// ------------------------------------------------------------------ -// -// last saved (in emacs): -// Time-stamp: <2009-August-12 15:35:30> -// -// ------------------------------------------------------------------ -// -// This module defines the GZipStream class, which can be used as a replacement for -// the System.IO.Compression.GZipStream class in the .NET BCL. NB: The design is not -// completely OO clean: there is some intelligence in the ZlibBaseStream that reads the -// GZip header. -// -// ------------------------------------------------------------------ - -#if WINDOWS_PHONE - -using System; -using System.IO; - -namespace RestSharp.Compression.ZLib -{ - /// - /// A class for compressing and decompressing GZIP streams. - /// - /// - /// - /// - /// The GZipStream is a Decorator on a . It adds GZIP compression or decompression to any stream. - /// - /// - /// Like the Compression.GZipStream in the .NET Base - /// Class Library, the Ionic.Zlib.GZipStream can compress while writing, or decompress - /// while reading, but not vice versa. The compression method used is GZIP, which is - /// documented in IETF RFC 1952, - /// "GZIP file format specification version 4.3". - /// - /// A GZipStream can be used to decompress data (through Read()) or to compress - /// data (through Write()), but not both. - /// - /// If you wish to use the GZipStream to compress data, you must wrap it around a - /// write-able stream. As you call Write() on the GZipStream, the data will be - /// compressed into the GZIP format. If you want to decompress data, you must wrap the - /// GZipStream around a readable stream that contains an IETF RFC 1952-compliant stream. - /// The data will be decompressed as you call Read() on the GZipStream. - /// - /// Though the GZIP format allows data from multiple files to be concatenated - /// together, this stream handles only a single segment of GZIP format, typically - /// representing a single file. - /// - /// - /// This class is similar to and . - /// ZlibStream handles RFC1950-compliant streams. - /// handles RFC1951-compliant streams. This class handles RFC1952-compliant streams. - /// - /// - /// - /// - /// - /// - internal class GZipStream : System.IO.Stream - { - // GZip format - // source: http://tools.ietf.org/html/rfc1952 - // - // header id: 2 bytes 1F 8B - // compress method 1 byte 8= DEFLATE (none other supported) - // flag 1 byte bitfield (See below) - // mtime 4 bytes time_t (seconds since jan 1, 1970 UTC of the file. - // xflg 1 byte 2 = max compress used , 4 = max speed (can be ignored) - // OS 1 byte OS for originating archive. set to 0xFF in compression. - // extra field length 2 bytes optional - only if FEXTRA is set. - // extra field varies - // filename varies optional - if FNAME is set. zero terminated. ISO-8859-1. - // file comment varies optional - if FCOMMENT is set. zero terminated. ISO-8859-1. - // crc16 1 byte optional - present only if FHCRC bit is set - // compressed data varies - // CRC32 4 bytes - // isize 4 bytes data size modulo 2^32 - // - // FLG (FLaGs) - // bit 0 FTEXT - indicates file is ASCII text (can be safely ignored) - // bit 1 FHCRC - there is a CRC16 for the header immediately following the header - // bit 2 FEXTRA - extra fields are present - // bit 3 FNAME - the zero-terminated filename is present. encoding; ISO-8859-1. - // bit 4 FCOMMENT - a zero-terminated file comment is present. encoding: ISO-8859-1 - // bit 5 reserved - // bit 6 reserved - // bit 7 reserved - // - // On consumption: - // Extra field is a bunch of nonsense and can be safely ignored. - // Header CRC and OS, likewise. - // - // on generation: - // all optional fields get 0, except for the OS, which gets 255. - // - - /// - /// The Comment on the GZIP stream. - /// - /// - /// - /// The GZIP format allows for each file to optionally have an associated comment stored with the - /// file. The comment is encoded with the ISO-8859-1 code page. To include a comment in - /// a GZIP stream you create, set this property before calling Write() for the first time - /// on the GZipStream. - /// - /// - /// - /// When using GZipStream to decompress, you can retrieve this property after the first - /// call to Read(). If no comment has been set in the GZIP bytestream, the Comment - /// property will return null (Nothing in VB). - /// - /// - public String Comment - { - get - { - return _Comment; - } - set - { - if (_disposed) throw new ObjectDisposedException("GZipStream"); - _Comment = value; - } - } - - /// - /// The FileName for the GZIP stream. - /// - /// - /// - /// The GZIP format optionally allows each file to have an associated filename. When - /// compressing data (through Write()), set this FileName before calling Write() the first - /// time on the GZipStream. The actual filename is encoded into the GZIP bytestream with - /// the ISO-8859-1 code page, according to RFC 1952. It is the application's responsibility to - /// insure that the FileName can be encoded and decoded correctly with this code page. - /// - /// - /// When decompressing (through Read()), you can retrieve this value any time after the - /// first Read(). In the case where there was no filename encoded into the GZIP - /// bytestream, the property will return null (Nothing in VB). - /// - /// - public String FileName - { - get { return _FileName; } - set - { - if (_disposed) throw new ObjectDisposedException("GZipStream"); - _FileName = value; - if (_FileName == null) return; - if (_FileName.IndexOf("/") != -1) - { - _FileName = _FileName.Replace("/", "\\"); - } - if (_FileName.EndsWith("\\")) - throw new Exception("Illegal filename"); - if (_FileName.IndexOf("\\") != -1) - { - // trim any leading path - _FileName = Path.GetFileName(_FileName); - } - } - } - - /// - /// The last modified time for the GZIP stream. - /// - /// - /// GZIP allows the storage of a last modified time with each GZIP entry. - /// When compressing data, you can set this before the first call to Write(). When - /// decompressing, you can retrieve this value any time after the first call to - /// Read(). - public DateTime? LastModified; - - /// - /// The CRC on the GZIP stream. - /// - /// - /// This is used for internal error checking. You probably don't need to look at this property. - /// - public int Crc32 { get { return _Crc32; } } - - internal ZlibBaseStream _baseStream; - bool _disposed; - bool _firstReadDone; - string _FileName; - string _Comment; - int _Crc32; - - - /// - /// Create a GZipStream using the specified CompressionMode and the specified CompressionLevel, - /// and explicitly specify whether the stream should be left open after Deflation or Inflation. - /// - /// - /// - /// This constructor allows the application to request that the captive stream remain open after - /// the deflation or inflation occurs. By default, after Close() is called on the stream, the - /// captive stream is also closed. In some cases this is not desired, for example if the stream - /// is a memory stream that will be re-read after compressed data has been written to it. Specify true for the - /// leaveOpen parameter to leave the stream open. - /// - /// - /// As noted in the class documentation, - /// the CompressionMode (Compress or Decompress) also establishes the "direction" of the stream. - /// A GZipStream with CompressionMode.Compress works only through Write(). A GZipStream with - /// CompressionMode.Decompress works only through Read(). - /// - /// - /// - /// This example shows how to use a DeflateStream to compress data. - /// - /// using (System.IO.Stream input = System.IO.File.OpenRead(fileToCompress)) - /// { - /// using (var raw = System.IO.File.Create(outputFile)) - /// { - /// using (Stream compressor = new GZipStream(raw, CompressionMode.Compress, CompressionLevel.BestCompression, true)) - /// { - /// byte[] buffer = new byte[WORKING_BUFFER_SIZE]; - /// int n; - /// while ((n= input.Read(buffer, 0, buffer.Length)) != 0) - /// { - /// compressor.Write(buffer, 0, n); - /// } - /// } - /// } - /// } - /// - /// - /// Dim outputFile As String = (fileToCompress & ".compressed") - /// Using input As Stream = File.OpenRead(fileToCompress) - /// Using raw As FileStream = File.Create(outputFile) - /// Using compressor As Stream = New GZipStream(raw, CompressionMode.Compress, CompressionLevel.BestCompression, True) - /// Dim buffer As Byte() = New Byte(4096) {} - /// Dim n As Integer = -1 - /// Do While (n <> 0) - /// If (n > 0) Then - /// compressor.Write(buffer, 0, n) - /// End If - /// n = input.Read(buffer, 0, buffer.Length) - /// Loop - /// End Using - /// End Using - /// End Using - /// - /// - /// The stream which will be read or written. - /// Indicates whether the GZipStream will compress or decompress. - /// true if the application would like the stream to remain open after inflation/deflation. - /// A tuning knob to trade speed for effectiveness. - public GZipStream(Stream stream) - { - _baseStream = new ZlibBaseStream(stream, ZlibStreamFlavor.GZIP, false); - } - - #region Zlib properties - - /// - /// This property sets the flush behavior on the stream. - /// - virtual public FlushType FlushMode - { - get { return (this._baseStream._flushMode); } - set - { - if (_disposed) throw new ObjectDisposedException("GZipStream"); - this._baseStream._flushMode = value; - } - } - - /// - /// The size of the working buffer for the compression codec. - /// - /// - /// - /// - /// The working buffer is used for all stream operations. The default size is 1024 bytes. - /// The minimum size is 128 bytes. You may get better performance with a larger buffer. - /// Then again, you might not. You would have to test it. - /// - /// - /// - /// Set this before the first call to Read() or Write() on the stream. If you try to set it - /// afterwards, it will throw. - /// - /// - public int BufferSize - { - get - { - return this._baseStream._bufferSize; - } - set - { - if (_disposed) throw new ObjectDisposedException("GZipStream"); - if (this._baseStream._workingBuffer != null) - throw new ZlibException("The working buffer is already set."); - if (value < ZlibConstants.WorkingBufferSizeMin) - throw new ZlibException(String.Format("Don't be silly. {0} bytes?? Use a bigger buffer.", value)); - this._baseStream._bufferSize = value; - } - } - - - /// Returns the total number of bytes input so far. - virtual public long TotalIn - { - get - { - return this._baseStream._z.TotalBytesIn; - } - } - - /// Returns the total number of bytes output so far. - virtual public long TotalOut - { - get - { - return this._baseStream._z.TotalBytesOut; - } - } - - #endregion - - #region Stream methods - - /// - /// Dispose the stream. - /// - /// - /// This may or may not result in a Close() call on the captive stream. - /// See the ctor's with leaveOpen parameters for more information. - /// - protected override void Dispose(bool disposing) - { - try - { - if (!_disposed) - { - if (disposing && (this._baseStream != null)) - { - this._baseStream.Close(); - this._Crc32 = _baseStream.Crc32; - } - _disposed = true; - } - } - finally - { - base.Dispose(disposing); - } - } - - - /// - /// Indicates whether the stream can be read. - /// - /// - /// The return value depends on whether the captive stream supports reading. - /// - public override bool CanRead - { - get - { - if (_disposed) throw new ObjectDisposedException("GZipStream"); - return _baseStream._stream.CanRead; - } - } - - /// - /// Indicates whether the stream supports Seek operations. - /// - /// - /// Always returns false. - /// - public override bool CanSeek - { - get { return false; } - } - - - /// - /// Indicates whether the stream can be written. - /// - /// - /// The return value depends on whether the captive stream supports writing. - /// - public override bool CanWrite - { - get - { - if (_disposed) throw new ObjectDisposedException("GZipStream"); - return _baseStream._stream.CanWrite; - } - } - - /// - /// Flush the stream. - /// - public override void Flush() - { - if (_disposed) throw new ObjectDisposedException("GZipStream"); - _baseStream.Flush(); - } - - /// - /// Reading this property always throws a NotImplementedException. - /// - public override long Length - { - get { throw new NotImplementedException(); } - } - - /// - /// The position of the stream pointer. - /// - /// - /// Writing this property always throws a NotImplementedException. Reading will - /// return the total bytes written out, if used in writing, or the total bytes - /// read in, if used in reading. The count may refer to compressed bytes or - /// uncompressed bytes, depending on how you've used the stream. - /// - public override long Position - { - get - { - if (this._baseStream._streamMode == ZlibBaseStream.StreamMode.Reader) - return this._baseStream._z.TotalBytesIn + this._baseStream._gzipHeaderByteCount; - return 0; - } - - set { throw new NotImplementedException(); } - } - - /// - /// Read and decompress data from the source stream. - /// - /// - /// With a GZipStream, decompression is done through reading. - /// - /// - /// - /// byte[] working = new byte[WORKING_BUFFER_SIZE]; - /// using (System.IO.Stream input = System.IO.File.OpenRead(_CompressedFile)) - /// { - /// using (Stream decompressor= new Ionic.Zlib.GZipStream(input, CompressionMode.Decompress, true)) - /// { - /// using (var output = System.IO.File.Create(_DecompressedFile)) - /// { - /// int n; - /// while ((n= decompressor.Read(working, 0, working.Length)) !=0) - /// { - /// output.Write(working, 0, n); - /// } - /// } - /// } - /// } - /// - /// - /// The buffer into which the decompressed data should be placed. - /// the offset within that data array to put the first byte read. - /// the number of bytes to read. - /// the number of bytes actually read - public override int Read(byte[] buffer, int offset, int count) - { - if (_disposed) throw new ObjectDisposedException("GZipStream"); - int n = _baseStream.Read(buffer, offset, count); - - // Console.WriteLine("GZipStream::Read(buffer, off({0}), c({1}) = {2}", offset, count, n); - // Console.WriteLine( Util.FormatByteArray(buffer, offset, n) ); - - if (!_firstReadDone) - { - _firstReadDone = true; - FileName = _baseStream._GzipFileName; - Comment = _baseStream._GzipComment; - } - return n; - } - - - - /// - /// Calling this method always throws a . - /// - /// irrelevant; it will always throw! - /// irrelevant; it will always throw! - /// irrelevant! - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotImplementedException(); - } - - /// - /// Calling this method always throws a NotImplementedException. - /// - /// irrelevant; this method will always throw! - public override void SetLength(long value) - { - throw new NotImplementedException(); - } - - #endregion - - - internal static System.DateTime _unixEpoch = new System.DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); - internal static System.Text.Encoding iso8859dash1 = System.Text.Encoding.GetEncoding("iso-8859-1"); - - - private int EmitHeader() - { - byte[] commentBytes = (Comment == null) ? null : iso8859dash1.GetBytes(Comment); - byte[] filenameBytes = (FileName == null) ? null : iso8859dash1.GetBytes(FileName); - - int cbLength = (Comment == null) ? 0 : commentBytes.Length + 1; - int fnLength = (FileName == null) ? 0 : filenameBytes.Length + 1; - - int bufferLength = 10 + cbLength + fnLength; - byte[] header = new byte[bufferLength]; - int i = 0; - // ID - header[i++] = 0x1F; - header[i++] = 0x8B; - - // compression method - header[i++] = 8; - byte flag = 0; - if (Comment != null) - flag ^= 0x10; - if (FileName != null) - flag ^= 0x8; - - // flag - header[i++] = flag; - - // mtime - if (!LastModified.HasValue) LastModified = DateTime.Now; - System.TimeSpan delta = LastModified.Value - _unixEpoch; - Int32 timet = (Int32)delta.TotalSeconds; - Array.Copy(BitConverter.GetBytes(timet), 0, header, i, 4); - i += 4; - - // xflg - header[i++] = 0; // this field is totally useless - // OS - header[i++] = 0xFF; // 0xFF == unspecified - - // extra field length - only if FEXTRA is set, which it is not. - //header[i++]= 0; - //header[i++]= 0; - - // filename - if (fnLength != 0) - { - Array.Copy(filenameBytes, 0, header, i, fnLength - 1); - i += fnLength - 1; - header[i++] = 0; // terminate - } - - // comment - if (cbLength != 0) - { - Array.Copy(commentBytes, 0, header, i, cbLength - 1); - i += cbLength - 1; - header[i++] = 0; // terminate - } - - _baseStream._stream.Write(header, 0, header.Length); - - return header.Length; // bytes written - } - - public override void Write(byte[] buffer, int offset, int count) - { - throw new NotImplementedException(); - } - } -} - -#endif \ No newline at end of file diff --git a/RestSharp/Compression/ZLib/InfTree.cs b/RestSharp/Compression/ZLib/InfTree.cs deleted file mode 100644 index bac57a9f1..000000000 --- a/RestSharp/Compression/ZLib/InfTree.cs +++ /dev/null @@ -1,442 +0,0 @@ -// Inftree.cs -// ------------------------------------------------------------------ -// -// Copyright (c) 2009 Dino Chiesa and Microsoft Corporation. -// All rights reserved. -// -// This code module is part of DotNetZip, a zipfile class library. -// -// ------------------------------------------------------------------ -// -// This code is licensed under the Microsoft Public License. -// See the file License.txt for the license details. -// More info on: http://dotnetzip.codeplex.com -// -// ------------------------------------------------------------------ -// -// last saved (in emacs): -// Time-stamp: <2009-May-31 09:17:05> -// -// ------------------------------------------------------------------ -// -// This module defines classes used in decompression. This code is derived -// from the jzlib implementation of zlib. In keeping with the license for jzlib, -// the copyright to that code is below. -// -// ------------------------------------------------------------------ -// -// Copyright (c) 2000,2001,2002,2003 ymnk, JCraft,Inc. All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright -// notice, this list of conditions and the following disclaimer in -// the documentation and/or other materials provided with the distribution. -// -// 3. The names of the authors may not be used to endorse or promote products -// derived from this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, -// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND -// FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT, -// INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, -// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, -// OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, -// EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -// -// ----------------------------------------------------------------------- -// -// This program is based on zlib-1.1.3; credit to authors -// Jean-loup Gailly(jloup@gzip.org) and Mark Adler(madler@alumni.caltech.edu) -// and contributors of zlib. -// -// ----------------------------------------------------------------------- - -#if WINDOWS_PHONE - -using System; -namespace RestSharp.Compression.ZLib -{ - - sealed internal class InfTree - { - - private const int MANY = 1440; - - private const int Z_OK = 0; - private const int Z_STREAM_END = 1; - private const int Z_NEED_DICT = 2; - private const int Z_ERRNO = -1; - private const int Z_STREAM_ERROR = -2; - private const int Z_DATA_ERROR = -3; - private const int Z_MEM_ERROR = -4; - private const int Z_BUF_ERROR = -5; - private const int Z_VERSION_ERROR = -6; - - internal const int fixed_bl = 9; - internal const int fixed_bd = 5; - - //UPGRADE_NOTE: Final was removed from the declaration of 'fixed_tl'. "ms-help://MS.VSCC.v80/dv_commoner/local/redirect.htm?index='!DefaultContextWindowIndex'&keyword='jlca1003'" - internal static readonly int[] fixed_tl = new int[]{96, 7, 256, 0, 8, 80, 0, 8, 16, 84, 8, 115, 82, 7, 31, 0, 8, 112, 0, 8, 48, 0, 9, 192, 80, 7, 10, 0, 8, 96, 0, 8, 32, 0, 9, 160, 0, 8, 0, 0, 8, 128, 0, 8, 64, 0, 9, 224, 80, 7, 6, 0, 8, 88, 0, 8, 24, 0, 9, 144, 83, 7, 59, 0, 8, 120, 0, 8, 56, 0, 9, 208, 81, 7, 17, 0, 8, 104, 0, 8, 40, 0, 9, 176, 0, 8, 8, 0, 8, 136, 0, 8, 72, 0, 9, 240, 80, 7, 4, 0, 8, 84, 0, 8, 20, 85, 8, 227, 83, 7, 43, 0, 8, 116, 0, 8, 52, 0, 9, 200, 81, 7, 13, 0, 8, 100, 0, 8, 36, 0, 9, 168, 0, 8, 4, 0, 8, 132, 0, 8, 68, 0, 9, 232, 80, 7, 8, 0, 8, 92, 0, 8, 28, 0, 9, 152, 84, 7, 83, 0, 8, 124, 0, 8, 60, 0, 9, 216, 82, 7, 23, 0, 8, 108, 0, 8, 44, 0, 9, 184, 0, 8, 12, 0, 8, 140, 0, 8, 76, 0, 9, 248, 80, 7, 3, 0, 8, 82, 0, 8, 18, 85, 8, 163, 83, 7, 35, 0, 8, 114, 0, 8, 50, 0, 9, 196, 81, 7, 11, 0, 8, 98, 0, 8, 34, 0, 9, 164, 0, 8, 2, 0, 8, 130, 0, 8, 66, 0, 9, 228, 80, 7, 7, 0, 8, 90, 0, 8, 26, 0, 9, 148, 84, 7, 67, 0, 8, 122, 0, 8, 58, 0, 9, 212, 82, 7, 19, 0, 8, 106, 0, 8, 42, 0, 9, 180, 0, 8, 10, 0, 8, 138, 0, 8, 74, 0, 9, 244, 80, 7, 5, 0, 8, 86, 0, 8, 22, 192, 8, 0, 83, 7, 51, 0, 8, 118, 0, 8, 54, 0, 9, 204, 81, 7, 15, 0, 8, 102, 0, 8, 38, 0, 9, 172, 0, 8, 6, 0, 8, 134, 0, 8, 70, 0, 9, 236, 80, 7, 9, 0, 8, 94, 0, 8, 30, 0, 9, 156, 84, 7, 99, 0, 8, 126, 0, 8, 62, 0, 9, 220, 82, 7, 27, 0, 8, 110, 0, 8, 46, 0, 9, 188, 0, 8, 14, 0, 8, 142, 0, 8, 78, 0, 9, 252, 96, 7, 256, 0, 8, 81, 0, 8, 17, 85, 8, 131, 82, 7, 31, 0, 8, 113, 0, 8, 49, 0, 9, 194, 80, 7, 10, 0, 8, 97, 0, 8, 33, 0, 9, 162, 0, 8, 1, 0, 8, 129, 0, 8, 65, 0, 9, 226, 80, 7, 6, 0, 8, 89, 0, 8, 25, 0, 9, 146, 83, 7, 59, 0, 8, 121, 0, 8, 57, 0, 9, 210, 81, 7, 17, 0, 8, 105, 0, 8, 41, 0, 9, 178, 0, 8, 9, 0, 8, 137, 0, 8, 73, 0, 9, 242, 80, 7, 4, 0, 8, 85, 0, 8, 21, 80, 8, 258, 83, 7, 43, 0, 8, 117, 0, 8, 53, 0, 9, 202, 81, 7, 13, 0, 8, 101, 0, 8, 37, 0, 9, 170, 0, 8, 5, 0, 8, 133, 0, 8, 69, 0, 9, 234, 80, 7, 8, 0, 8, 93, 0, 8, 29, 0, 9, 154, 84, 7, 83, 0, 8, 125, 0, 8, 61, 0, 9, 218, 82, 7, 23, 0, 8, 109, 0, 8, 45, 0, 9, 186, - 0, 8, 13, 0, 8, 141, 0, 8, 77, 0, 9, 250, 80, 7, 3, 0, 8, 83, 0, 8, 19, 85, 8, 195, 83, 7, 35, 0, 8, 115, 0, 8, 51, 0, 9, 198, 81, 7, 11, 0, 8, 99, 0, 8, 35, 0, 9, 166, 0, 8, 3, 0, 8, 131, 0, 8, 67, 0, 9, 230, 80, 7, 7, 0, 8, 91, 0, 8, 27, 0, 9, 150, 84, 7, 67, 0, 8, 123, 0, 8, 59, 0, 9, 214, 82, 7, 19, 0, 8, 107, 0, 8, 43, 0, 9, 182, 0, 8, 11, 0, 8, 139, 0, 8, 75, 0, 9, 246, 80, 7, 5, 0, 8, 87, 0, 8, 23, 192, 8, 0, 83, 7, 51, 0, 8, 119, 0, 8, 55, 0, 9, 206, 81, 7, 15, 0, 8, 103, 0, 8, 39, 0, 9, 174, 0, 8, 7, 0, 8, 135, 0, 8, 71, 0, 9, 238, 80, 7, 9, 0, 8, 95, 0, 8, 31, 0, 9, 158, 84, 7, 99, 0, 8, 127, 0, 8, 63, 0, 9, 222, 82, 7, 27, 0, 8, 111, 0, 8, 47, 0, 9, 190, 0, 8, 15, 0, 8, 143, 0, 8, 79, 0, 9, 254, 96, 7, 256, 0, 8, 80, 0, 8, 16, 84, 8, 115, 82, 7, 31, 0, 8, 112, 0, 8, 48, 0, 9, 193, 80, 7, 10, 0, 8, 96, 0, 8, 32, 0, 9, 161, 0, 8, 0, 0, 8, 128, 0, 8, 64, 0, 9, 225, 80, 7, 6, 0, 8, 88, 0, 8, 24, 0, 9, 145, 83, 7, 59, 0, 8, 120, 0, 8, 56, 0, 9, 209, 81, 7, 17, 0, 8, 104, 0, 8, 40, 0, 9, 177, 0, 8, 8, 0, 8, 136, 0, 8, 72, 0, 9, 241, 80, 7, 4, 0, 8, 84, 0, 8, 20, 85, 8, 227, 83, 7, 43, 0, 8, 116, 0, 8, 52, 0, 9, 201, 81, 7, 13, 0, 8, 100, 0, 8, 36, 0, 9, 169, 0, 8, 4, 0, 8, 132, 0, 8, 68, 0, 9, 233, 80, 7, 8, 0, 8, 92, 0, 8, 28, 0, 9, 153, 84, 7, 83, 0, 8, 124, 0, 8, 60, 0, 9, 217, 82, 7, 23, 0, 8, 108, 0, 8, 44, 0, 9, 185, 0, 8, 12, 0, 8, 140, 0, 8, 76, 0, 9, 249, 80, 7, 3, 0, 8, 82, 0, 8, 18, 85, 8, 163, 83, 7, 35, 0, 8, 114, 0, 8, 50, 0, 9, 197, 81, 7, 11, 0, 8, 98, 0, 8, 34, 0, 9, 165, 0, 8, 2, 0, 8, 130, 0, 8, 66, 0, 9, 229, 80, 7, 7, 0, 8, 90, 0, 8, 26, 0, 9, 149, 84, 7, 67, 0, 8, 122, 0, 8, 58, 0, 9, 213, 82, 7, 19, 0, 8, 106, 0, 8, 42, 0, 9, 181, 0, 8, 10, 0, 8, 138, 0, 8, 74, 0, 9, 245, 80, 7, 5, 0, 8, 86, 0, 8, 22, 192, 8, 0, 83, 7, 51, 0, 8, 118, 0, 8, 54, 0, 9, 205, 81, 7, 15, 0, 8, 102, 0, 8, 38, 0, 9, 173, 0, 8, 6, 0, 8, 134, 0, 8, 70, 0, 9, 237, 80, 7, 9, 0, 8, 94, 0, 8, 30, 0, 9, 157, 84, 7, 99, 0, 8, 126, 0, 8, 62, 0, 9, 221, 82, 7, 27, 0, 8, 110, 0, 8, 46, 0, 9, 189, 0, 8, - 14, 0, 8, 142, 0, 8, 78, 0, 9, 253, 96, 7, 256, 0, 8, 81, 0, 8, 17, 85, 8, 131, 82, 7, 31, 0, 8, 113, 0, 8, 49, 0, 9, 195, 80, 7, 10, 0, 8, 97, 0, 8, 33, 0, 9, 163, 0, 8, 1, 0, 8, 129, 0, 8, 65, 0, 9, 227, 80, 7, 6, 0, 8, 89, 0, 8, 25, 0, 9, 147, 83, 7, 59, 0, 8, 121, 0, 8, 57, 0, 9, 211, 81, 7, 17, 0, 8, 105, 0, 8, 41, 0, 9, 179, 0, 8, 9, 0, 8, 137, 0, 8, 73, 0, 9, 243, 80, 7, 4, 0, 8, 85, 0, 8, 21, 80, 8, 258, 83, 7, 43, 0, 8, 117, 0, 8, 53, 0, 9, 203, 81, 7, 13, 0, 8, 101, 0, 8, 37, 0, 9, 171, 0, 8, 5, 0, 8, 133, 0, 8, 69, 0, 9, 235, 80, 7, 8, 0, 8, 93, 0, 8, 29, 0, 9, 155, 84, 7, 83, 0, 8, 125, 0, 8, 61, 0, 9, 219, 82, 7, 23, 0, 8, 109, 0, 8, 45, 0, 9, 187, 0, 8, 13, 0, 8, 141, 0, 8, 77, 0, 9, 251, 80, 7, 3, 0, 8, 83, 0, 8, 19, 85, 8, 195, 83, 7, 35, 0, 8, 115, 0, 8, 51, 0, 9, 199, 81, 7, 11, 0, 8, 99, 0, 8, 35, 0, 9, 167, 0, 8, 3, 0, 8, 131, 0, 8, 67, 0, 9, 231, 80, 7, 7, 0, 8, 91, 0, 8, 27, 0, 9, 151, 84, 7, 67, 0, 8, 123, 0, 8, 59, 0, 9, 215, 82, 7, 19, 0, 8, 107, 0, 8, 43, 0, 9, 183, 0, 8, 11, 0, 8, 139, 0, 8, 75, 0, 9, 247, 80, 7, 5, 0, 8, 87, 0, 8, 23, 192, 8, 0, 83, 7, 51, 0, 8, 119, 0, 8, 55, 0, 9, 207, 81, 7, 15, 0, 8, 103, 0, 8, 39, 0, 9, 175, 0, 8, 7, 0, 8, 135, 0, 8, 71, 0, 9, 239, 80, 7, 9, 0, 8, 95, 0, 8, 31, 0, 9, 159, 84, 7, 99, 0, 8, 127, 0, 8, 63, 0, 9, 223, 82, 7, 27, 0, 8, 111, 0, 8, 47, 0, 9, 191, 0, 8, 15, 0, 8, 143, 0, 8, 79, 0, 9, 255}; - //UPGRADE_NOTE: Final was removed from the declaration of 'fixed_td'. "ms-help://MS.VSCC.v80/dv_commoner/local/redirect.htm?index='!DefaultContextWindowIndex'&keyword='jlca1003'" - internal static readonly int[] fixed_td = new int[] { 80, 5, 1, 87, 5, 257, 83, 5, 17, 91, 5, 4097, 81, 5, 5, 89, 5, 1025, 85, 5, 65, 93, 5, 16385, 80, 5, 3, 88, 5, 513, 84, 5, 33, 92, 5, 8193, 82, 5, 9, 90, 5, 2049, 86, 5, 129, 192, 5, 24577, 80, 5, 2, 87, 5, 385, 83, 5, 25, 91, 5, 6145, 81, 5, 7, 89, 5, 1537, 85, 5, 97, 93, 5, 24577, 80, 5, 4, 88, 5, 769, 84, 5, 49, 92, 5, 12289, 82, 5, 13, 90, 5, 3073, 86, 5, 193, 192, 5, 24577 }; - - // Tables for deflate from PKZIP's appnote.txt. - //UPGRADE_NOTE: Final was removed from the declaration of 'cplens'. "ms-help://MS.VSCC.v80/dv_commoner/local/redirect.htm?index='!DefaultContextWindowIndex'&keyword='jlca1003'" - internal static readonly int[] cplens = new int[] { 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258, 0, 0 }; - - // see note #13 above about 258 - //UPGRADE_NOTE: Final was removed from the declaration of 'cplext'. "ms-help://MS.VSCC.v80/dv_commoner/local/redirect.htm?index='!DefaultContextWindowIndex'&keyword='jlca1003'" - internal static readonly int[] cplext = new int[] { 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, 112, 112 }; - - //UPGRADE_NOTE: Final was removed from the declaration of 'cpdist'. "ms-help://MS.VSCC.v80/dv_commoner/local/redirect.htm?index='!DefaultContextWindowIndex'&keyword='jlca1003'" - internal static readonly int[] cpdist = new int[] { 1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577 }; - - //UPGRADE_NOTE: Final was removed from the declaration of 'cpdext'. "ms-help://MS.VSCC.v80/dv_commoner/local/redirect.htm?index='!DefaultContextWindowIndex'&keyword='jlca1003'" - internal static readonly int[] cpdext = new int[] { 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13 }; - - // If BMAX needs to be larger than 16, then h and x[] should be uLong. - internal const int BMAX = 15; // maximum bit length of any code - - internal int[] hn = null; // hufts used in space - internal int[] v = null; // work area for huft_build - internal int[] c = null; // bit length count table - internal int[] r = null; // table entry for structure assignment - internal int[] u = null; // table stack - internal int[] x = null; // bit offsets, then code stack - - private int huft_build(int[] b, int bindex, int n, int s, int[] d, int[] e, int[] t, int[] m, int[] hp, int[] hn, int[] v) - { - // Given a list of code lengths and a maximum table size, make a set of - // tables to decode that set of codes. Return Z_OK on success, Z_BUF_ERROR - // if the given code set is incomplete (the tables are still built in this - // case), Z_DATA_ERROR if the input is invalid (an over-subscribed set of - // lengths), or Z_MEM_ERROR if not enough memory. - - int a; // counter for codes of length k - int f; // i repeats in table every f entries - int g; // maximum code length - int h; // table level - int i; // counter, current code - int j; // counter - int k; // number of bits in current code - int l; // bits per table (returned in m) - int mask; // (1 << w) - 1, to avoid cc -O bug on HP - int p; // pointer into c[], b[], or v[] - int q; // points to current table - int w; // bits before this table == (l * h) - int xp; // pointer into x - int y; // number of dummy codes added - int z; // number of entries in current table - - // Generate counts for each bit length - - p = 0; i = n; - do - { - c[b[bindex + p]]++; p++; i--; // assume all entries <= BMAX - } - while (i != 0); - - if (c[0] == n) - { - // null input--all zero length codes - t[0] = -1; - m[0] = 0; - return Z_OK; - } - - // Find minimum and maximum length, bound *m by those - l = m[0]; - for (j = 1; j <= BMAX; j++) - if (c[j] != 0) - break; - k = j; // minimum code length - if (l < j) - { - l = j; - } - for (i = BMAX; i != 0; i--) - { - if (c[i] != 0) - break; - } - g = i; // maximum code length - if (l > i) - { - l = i; - } - m[0] = l; - - // Adjust last length count to fill out codes, if needed - for (y = 1 << j; j < i; j++, y <<= 1) - { - if ((y -= c[j]) < 0) - { - return Z_DATA_ERROR; - } - } - if ((y -= c[i]) < 0) - { - return Z_DATA_ERROR; - } - c[i] += y; - - // Generate starting offsets into the value table for each length - x[1] = j = 0; - p = 1; xp = 2; - while (--i != 0) - { - // note that i == g from above - x[xp] = (j += c[p]); - xp++; - p++; - } - - // Make a table of values in order of bit lengths - i = 0; p = 0; - do - { - if ((j = b[bindex + p]) != 0) - { - v[x[j]++] = i; - } - p++; - } - while (++i < n); - n = x[g]; // set n to length of v - - // Generate the Huffman codes and for each, make the table entries - x[0] = i = 0; // first Huffman code is zero - p = 0; // grab values in bit order - h = -1; // no tables yet--level -1 - w = -l; // bits decoded == (l * h) - u[0] = 0; // just to keep compilers happy - q = 0; // ditto - z = 0; // ditto - - // go through the bit lengths (k already is bits in shortest code) - for (; k <= g; k++) - { - a = c[k]; - while (a-- != 0) - { - // here i is the Huffman code of length k bits for value *p - // make tables up to required level - while (k > w + l) - { - h++; - w += l; // previous table always l bits - // compute minimum size table less than or equal to l bits - z = g - w; - z = (z > l) ? l : z; // table size upper limit - if ((f = 1 << (j = k - w)) > a + 1) - { - // try a k-w bit table - // too few codes for k-w bit table - f -= (a + 1); // deduct codes from patterns left - xp = k; - if (j < z) - { - while (++j < z) - { - // try smaller tables up to z bits - if ((f <<= 1) <= c[++xp]) - break; // enough codes to use up j bits - f -= c[xp]; // else deduct codes from patterns - } - } - } - z = 1 << j; // table entries for j-bit table - - // allocate new table - if (hn[0] + z > MANY) - { - // (note: doesn't matter for fixed) - return Z_DATA_ERROR; // overflow of MANY - } - u[h] = q = hn[0]; // DEBUG - hn[0] += z; - - // connect to last table, if there is one - if (h != 0) - { - x[h] = i; // save pattern for backing up - r[0] = (sbyte)j; // bits in this table - r[1] = (sbyte)l; // bits to dump before this table - j = SharedUtils.URShift(i, (w - l)); - r[2] = (int)(q - u[h - 1] - j); // offset to this table - Array.Copy(r, 0, hp, (u[h - 1] + j) * 3, 3); // connect to last table - } - else - { - t[0] = q; // first table is returned result - } - } - - // set up table entry in r - r[1] = (sbyte)(k - w); - if (p >= n) - { - r[0] = 128 + 64; // out of values--invalid code - } - else if (v[p] < s) - { - r[0] = (sbyte)(v[p] < 256 ? 0 : 32 + 64); // 256 is end-of-block - r[2] = v[p++]; // simple code is just the value - } - else - { - r[0] = (sbyte)(e[v[p] - s] + 16 + 64); // non-simple--look up in lists - r[2] = d[v[p++] - s]; - } - - // fill code-like entries with r - f = 1 << (k - w); - for (j = SharedUtils.URShift(i, w); j < z; j += f) - { - Array.Copy(r, 0, hp, (q + j) * 3, 3); - } - - // backwards increment the k-bit code i - for (j = 1 << (k - 1); (i & j) != 0; j = SharedUtils.URShift(j, 1)) - { - i ^= j; - } - i ^= j; - - // backup over finished tables - mask = (1 << w) - 1; // needed on HP, cc -O bug - while ((i & mask) != x[h]) - { - h--; // don't need to update q - w -= l; - mask = (1 << w) - 1; - } - } - } - // Return Z_BUF_ERROR if we were given an incomplete table - return y != 0 && g != 1 ? Z_BUF_ERROR : Z_OK; - } - - internal int inflate_trees_bits(int[] c, int[] bb, int[] tb, int[] hp, ZlibCodec z) - { - int result; - initWorkArea(19); - hn[0] = 0; - result = huft_build(c, 0, 19, 19, null, null, tb, bb, hp, hn, v); - - if (result == Z_DATA_ERROR) - { - z.Message = "oversubscribed dynamic bit lengths tree"; - } - else if (result == Z_BUF_ERROR || bb[0] == 0) - { - z.Message = "incomplete dynamic bit lengths tree"; - result = Z_DATA_ERROR; - } - return result; - } - - internal int inflate_trees_dynamic(int nl, int nd, int[] c, int[] bl, int[] bd, int[] tl, int[] td, int[] hp, ZlibCodec z) - { - int result; - - // build literal/length tree - initWorkArea(288); - hn[0] = 0; - result = huft_build(c, 0, nl, 257, cplens, cplext, tl, bl, hp, hn, v); - if (result != Z_OK || bl[0] == 0) - { - if (result == Z_DATA_ERROR) - { - z.Message = "oversubscribed literal/length tree"; - } - else if (result != Z_MEM_ERROR) - { - z.Message = "incomplete literal/length tree"; - result = Z_DATA_ERROR; - } - return result; - } - - // build distance tree - initWorkArea(288); - result = huft_build(c, nl, nd, 0, cpdist, cpdext, td, bd, hp, hn, v); - - if (result != Z_OK || (bd[0] == 0 && nl > 257)) - { - if (result == Z_DATA_ERROR) - { - z.Message = "oversubscribed distance tree"; - } - else if (result == Z_BUF_ERROR) - { - z.Message = "incomplete distance tree"; - result = Z_DATA_ERROR; - } - else if (result != Z_MEM_ERROR) - { - z.Message = "empty distance tree with lengths"; - result = Z_DATA_ERROR; - } - return result; - } - - return Z_OK; - } - - internal static int inflate_trees_fixed(int[] bl, int[] bd, int[][] tl, int[][] td, ZlibCodec z) - { - bl[0] = fixed_bl; - bd[0] = fixed_bd; - tl[0] = fixed_tl; - td[0] = fixed_td; - return Z_OK; - } - - private void initWorkArea(int vsize) - { - if (hn == null) - { - hn = new int[1]; - v = new int[vsize]; - c = new int[BMAX + 1]; - r = new int[3]; - u = new int[BMAX]; - x = new int[BMAX + 1]; - } - if (v.Length < vsize) - { - v = new int[vsize]; - } - for (int i = 0; i < vsize; i++) - { - v[i] = 0; - } - for (int i = 0; i < BMAX + 1; i++) - { - c[i] = 0; - } - for (int i = 0; i < 3; i++) - { - r[i] = 0; - } - // for(int i=0; i -// -// ------------------------------------------------------------------ -// -// This module defines classes for decompression. This code is derived -// from the jzlib implementation of zlib, but significantly modified. -// The object model is not the same, and many of the behaviors are -// different. Nonetheless, in keeping with the license for jzlib, I am -// reproducing the copyright to that code here. -// -// ------------------------------------------------------------------ -// -// Copyright (c) 2000,2001,2002,2003 ymnk, JCraft,Inc. All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright -// notice, this list of conditions and the following disclaimer in -// the documentation and/or other materials provided with the distribution. -// -// 3. The names of the authors may not be used to endorse or promote products -// derived from this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, -// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND -// FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT, -// INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, -// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, -// OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, -// EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -// -// ----------------------------------------------------------------------- -// -// This program is based on zlib-1.1.3; credit to authors -// Jean-loup Gailly(jloup@gzip.org) and Mark Adler(madler@alumni.caltech.edu) -// and contributors of zlib. -// -// ----------------------------------------------------------------------- - -#if WINDOWS_PHONE - -using System; -namespace RestSharp.Compression.ZLib -{ - sealed internal class InflateBlocks - { - private const int MANY = 1440; - - // And'ing with mask[n] masks the lower n bits - //UPGRADE_NOTE: Final was removed from the declaration of 'inflate_mask'. "ms-help://MS.VSCC.v80/dv_commoner/local/redirect.htm?index='!DefaultContextWindowIndex'&keyword='jlca1003'" - private static readonly int[] inflate_mask = new int[] { 0x00000000, 0x00000001, 0x00000003, 0x00000007, 0x0000000f, 0x0000001f, 0x0000003f, 0x0000007f, 0x000000ff, 0x000001ff, 0x000003ff, 0x000007ff, 0x00000fff, 0x00001fff, 0x00003fff, 0x00007fff, 0x0000ffff }; - - // Table for deflate from PKZIP's appnote.txt. - //UPGRADE_NOTE: Final was removed from the declaration of 'border'. "ms-help://MS.VSCC.v80/dv_commoner/local/redirect.htm?index='!DefaultContextWindowIndex'&keyword='jlca1003'" - internal static readonly int[] border = new int[] { 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 }; - - private const int TYPE = 0; // get type bits (3, including end bit) - private const int LENS = 1; // get lengths for stored - private const int STORED = 2; // processing stored block - private const int TABLE = 3; // get table lengths - private const int BTREE = 4; // get bit lengths tree for a dynamic block - private const int DTREE = 5; // get length, distance trees for a dynamic block - private const int CODES = 6; // processing fixed or dynamic block - private const int DRY = 7; // output remaining window bytes - private const int DONE = 8; // finished last block, done - private const int BAD = 9; // ot a data error--stuck here - - internal int mode; // current inflate_block mode - - internal int left; // if STORED, bytes left to copy - - internal int table; // table lengths (14 bits) - internal int index; // index into blens (or border) - internal int[] blens; // bit lengths of codes - internal int[] bb = new int[1]; // bit length tree depth - internal int[] tb = new int[1]; // bit length decoding tree - - internal InflateCodes codes = new InflateCodes(); // if CODES, current state - - internal int last; // true if this block is the last block - - internal ZlibCodec _codec; // pointer back to this zlib stream - - // mode independent information - internal int bitk; // bits in bit buffer - internal int bitb; // bit buffer - internal int[] hufts; // single malloc for tree space - internal byte[] window; // sliding window - internal int end; // one byte after sliding window - internal int read; // window read pointer - internal int write; // window write pointer - internal System.Object checkfn; // check function - internal long check; // check on output - - internal InfTree inftree = new InfTree(); - - internal InflateBlocks(ZlibCodec codec, System.Object checkfn, int w) - { - _codec = codec; - hufts = new int[MANY * 3]; - window = new byte[w]; - end = w; - this.checkfn = checkfn; - mode = TYPE; - Reset(null); - } - - internal void Reset(long[] c) - { - if (c != null) - c[0] = check; - if (mode == BTREE || mode == DTREE) - { - } - if (mode == CODES) - { - } - mode = TYPE; - bitk = 0; - bitb = 0; - read = write = 0; - - if (checkfn != null) - _codec._Adler32 = check = Adler.Adler32(0L, null, 0, 0); - } - - internal int Process(int r) - { - int t; // temporary storage - int b; // bit buffer - int k; // bits in bit buffer - int p; // input data pointer - int n; // bytes available there - int q; // output window write pointer - int m; // bytes to end of window or read pointer - - // copy input/output information to locals (UPDATE macro restores) - - p = _codec.NextIn; - n = _codec.AvailableBytesIn; - b = bitb; - k = bitk; - - q = write; - m = (int)(q < read ? read - q - 1 : end - q); - - - // process input based on current state - while (true) - { - switch (mode) - { - - case TYPE: - - while (k < (3)) - { - if (n != 0) - { - r = ZlibConstants.Z_OK; - } - else - { - bitb = b; bitk = k; - _codec.AvailableBytesIn = n; - _codec.TotalBytesIn += p - _codec.NextIn; - _codec.NextIn = p; - write = q; - return Flush(r); - } - - n--; - b |= (_codec.InputBuffer[p++] & 0xff) << k; - k += 8; - } - t = (int)(b & 7); - last = t & 1; - - switch (SharedUtils.URShift(t, 1)) - { - case 0: // stored - b = SharedUtils.URShift(b, (3)); k -= (3); - t = k & 7; // go to byte boundary - b = SharedUtils.URShift(b, (t)); k -= (t); - mode = LENS; // get length of stored block - break; - - case 1: // fixed - int[] bl = new int[1]; - int[] bd = new int[1]; - int[][] tl = new int[1][]; - int[][] td = new int[1][]; - InfTree.inflate_trees_fixed(bl, bd, tl, td, _codec); - codes.Init(bl[0], bd[0], tl[0], 0, td[0], 0); - b = SharedUtils.URShift(b, (3)); k -= (3); - mode = CODES; - break; - - case 2: // dynamic - b = SharedUtils.URShift(b, (3)); k -= (3); - mode = TABLE; - break; - - case 3: // illegal - b = SharedUtils.URShift(b, (3)); k -= (3); - mode = BAD; - _codec.Message = "invalid block type"; - r = ZlibConstants.Z_DATA_ERROR; - bitb = b; bitk = k; - _codec.AvailableBytesIn = n; - _codec.TotalBytesIn += p - _codec.NextIn; - _codec.NextIn = p; - write = q; - return Flush(r); - } - break; - - case LENS: - - while (k < (32)) - { - if (n != 0) - { - r = ZlibConstants.Z_OK; - } - else - { - bitb = b; bitk = k; - _codec.AvailableBytesIn = n; - _codec.TotalBytesIn += p - _codec.NextIn; - _codec.NextIn = p; - write = q; - return Flush(r); - } - ; - n--; - b |= (_codec.InputBuffer[p++] & 0xff) << k; - k += 8; - } - - if (((SharedUtils.URShift((~b), 16)) & 0xffff) != (b & 0xffff)) - { - mode = BAD; - _codec.Message = "invalid stored block lengths"; - r = ZlibConstants.Z_DATA_ERROR; - - bitb = b; bitk = k; - _codec.AvailableBytesIn = n; - _codec.TotalBytesIn += p - _codec.NextIn; - _codec.NextIn = p; - write = q; - return Flush(r); - } - left = (b & 0xffff); - b = k = 0; // dump bits - mode = left != 0 ? STORED : (last != 0 ? DRY : TYPE); - break; - - case STORED: - if (n == 0) - { - bitb = b; bitk = k; - _codec.AvailableBytesIn = n; - _codec.TotalBytesIn += p - _codec.NextIn; - _codec.NextIn = p; - write = q; - return Flush(r); - } - - if (m == 0) - { - if (q == end && read != 0) - { - q = 0; m = (int)(q < read ? read - q - 1 : end - q); - } - if (m == 0) - { - write = q; - r = Flush(r); - q = write; m = (int)(q < read ? read - q - 1 : end - q); - if (q == end && read != 0) - { - q = 0; m = (int)(q < read ? read - q - 1 : end - q); - } - if (m == 0) - { - bitb = b; bitk = k; - _codec.AvailableBytesIn = n; - _codec.TotalBytesIn += p - _codec.NextIn; - _codec.NextIn = p; - write = q; - return Flush(r); - } - } - } - r = ZlibConstants.Z_OK; - - t = left; - if (t > n) - t = n; - if (t > m) - t = m; - Array.Copy(_codec.InputBuffer, p, window, q, t); - p += t; n -= t; - q += t; m -= t; - if ((left -= t) != 0) - break; - mode = last != 0 ? DRY : TYPE; - break; - - case TABLE: - - while (k < (14)) - { - if (n != 0) - { - r = ZlibConstants.Z_OK; - } - else - { - bitb = b; bitk = k; - _codec.AvailableBytesIn = n; - _codec.TotalBytesIn += p - _codec.NextIn; - _codec.NextIn = p; - write = q; - return Flush(r); - } - ; - n--; - b |= (_codec.InputBuffer[p++] & 0xff) << k; - k += 8; - } - - table = t = (b & 0x3fff); - if ((t & 0x1f) > 29 || ((t >> 5) & 0x1f) > 29) - { - mode = BAD; - _codec.Message = "too many length or distance symbols"; - r = ZlibConstants.Z_DATA_ERROR; - - bitb = b; bitk = k; - _codec.AvailableBytesIn = n; - _codec.TotalBytesIn += p - _codec.NextIn; - _codec.NextIn = p; - write = q; - return Flush(r); - } - t = 258 + (t & 0x1f) + ((t >> 5) & 0x1f); - if (blens == null || blens.Length < t) - { - blens = new int[t]; - } - else - { - for (int i = 0; i < t; i++) - { - blens[i] = 0; - } - } - { - b = SharedUtils.URShift(b, (14)); k -= (14); - } - - index = 0; - mode = BTREE; - goto case BTREE; - - case BTREE: - while (index < 4 + (SharedUtils.URShift(table, 10))) - { - while (k < (3)) - { - if (n != 0) - { - r = ZlibConstants.Z_OK; - } - else - { - bitb = b; bitk = k; - _codec.AvailableBytesIn = n; - _codec.TotalBytesIn += p - _codec.NextIn; - _codec.NextIn = p; - write = q; - return Flush(r); - } - ; - n--; - b |= (_codec.InputBuffer[p++] & 0xff) << k; - k += 8; - } - - blens[border[index++]] = b & 7; - - { - b = SharedUtils.URShift(b, (3)); k -= (3); - } - } - - while (index < 19) - { - blens[border[index++]] = 0; - } - - bb[0] = 7; - t = inftree.inflate_trees_bits(blens, bb, tb, hufts, _codec); - if (t != ZlibConstants.Z_OK) - { - r = t; - if (r == ZlibConstants.Z_DATA_ERROR) - { - blens = null; - mode = BAD; - } - - bitb = b; bitk = k; - _codec.AvailableBytesIn = n; - _codec.TotalBytesIn += p - _codec.NextIn; - _codec.NextIn = p; - write = q; - return Flush(r); - } - - index = 0; - mode = DTREE; - goto case DTREE; - - case DTREE: - while (true) - { - t = table; - if (!(index < 258 + (t & 0x1f) + ((t >> 5) & 0x1f))) - { - break; - } - - int i, j, c; - - t = bb[0]; - - while (k < (t)) - { - if (n != 0) - { - r = ZlibConstants.Z_OK; - } - else - { - bitb = b; bitk = k; - _codec.AvailableBytesIn = n; - _codec.TotalBytesIn += p - _codec.NextIn; - _codec.NextIn = p; - write = q; - return Flush(r); - } - ; - n--; - b |= (_codec.InputBuffer[p++] & 0xff) << k; - k += 8; - } - - if (tb[0] == -1) - { - //System.err.println("null..."); - } - - t = hufts[(tb[0] + (b & inflate_mask[t])) * 3 + 1]; - c = hufts[(tb[0] + (b & inflate_mask[t])) * 3 + 2]; - - if (c < 16) - { - b = SharedUtils.URShift(b, (t)); k -= (t); - blens[index++] = c; - } - else - { - // c == 16..18 - i = c == 18 ? 7 : c - 14; - j = c == 18 ? 11 : 3; - - while (k < (t + i)) - { - if (n != 0) - { - r = ZlibConstants.Z_OK; - } - else - { - bitb = b; bitk = k; - _codec.AvailableBytesIn = n; - _codec.TotalBytesIn += p - _codec.NextIn; - _codec.NextIn = p; - write = q; - return Flush(r); - } - ; - n--; - b |= (_codec.InputBuffer[p++] & 0xff) << k; - k += 8; - } - - b = SharedUtils.URShift(b, (t)); k -= (t); - - j += (b & inflate_mask[i]); - - b = SharedUtils.URShift(b, (i)); k -= (i); - - i = index; - t = table; - if (i + j > 258 + (t & 0x1f) + ((t >> 5) & 0x1f) || (c == 16 && i < 1)) - { - blens = null; - mode = BAD; - _codec.Message = "invalid bit length repeat"; - r = ZlibConstants.Z_DATA_ERROR; - - bitb = b; bitk = k; - _codec.AvailableBytesIn = n; - _codec.TotalBytesIn += p - _codec.NextIn; - _codec.NextIn = p; - write = q; - return Flush(r); - } - - c = c == 16 ? blens[i - 1] : 0; - do - { - blens[i++] = c; - } - while (--j != 0); - index = i; - } - } - - tb[0] = -1; - { - int[] bl = new int[] { 9 }; // must be <= 9 for lookahead assumptions - int[] bd = new int[] { 6 }; // must be <= 9 for lookahead assumptions - int[] tl = new int[1]; - int[] td = new int[1]; - - t = table; - t = inftree.inflate_trees_dynamic(257 + (t & 0x1f), 1 + ((t >> 5) & 0x1f), blens, bl, bd, tl, td, hufts, _codec); - - if (t != ZlibConstants.Z_OK) - { - if (t == ZlibConstants.Z_DATA_ERROR) - { - blens = null; - mode = BAD; - } - r = t; - - bitb = b; bitk = k; - _codec.AvailableBytesIn = n; - _codec.TotalBytesIn += p - _codec.NextIn; - _codec.NextIn = p; - write = q; - return Flush(r); - } - codes.Init(bl[0], bd[0], hufts, tl[0], hufts, td[0]); - } - mode = CODES; - goto case CODES; - - case CODES: - bitb = b; bitk = k; - _codec.AvailableBytesIn = n; - _codec.TotalBytesIn += p - _codec.NextIn; - _codec.NextIn = p; - write = q; - - if ((r = codes.Process(this, r)) != ZlibConstants.Z_STREAM_END) - return Flush(r); - - r = ZlibConstants.Z_OK; - p = _codec.NextIn; - n = _codec.AvailableBytesIn; - b = bitb; - k = bitk; - q = write; - m = (int)(q < read ? read - q - 1 : end - q); - - if (last == 0) - { - mode = TYPE; - break; - } - mode = DRY; - goto case DRY; - - case DRY: - write = q; - r = Flush(r); - q = write; m = (int)(q < read ? read - q - 1 : end - q); - if (read != write) - { - bitb = b; bitk = k; - _codec.AvailableBytesIn = n; - _codec.TotalBytesIn += p - _codec.NextIn; - _codec.NextIn = p; - write = q; - return Flush(r); - } - mode = DONE; - goto case DONE; - - case DONE: - r = ZlibConstants.Z_STREAM_END; - bitb = b; - bitk = k; - _codec.AvailableBytesIn = n; - _codec.TotalBytesIn += p - _codec.NextIn; - _codec.NextIn = p; - write = q; - return Flush(r); - - case BAD: - r = ZlibConstants.Z_DATA_ERROR; - - bitb = b; bitk = k; - _codec.AvailableBytesIn = n; - _codec.TotalBytesIn += p - _codec.NextIn; - _codec.NextIn = p; - write = q; - return Flush(r); - - - default: - r = ZlibConstants.Z_STREAM_ERROR; - - bitb = b; bitk = k; - _codec.AvailableBytesIn = n; - _codec.TotalBytesIn += p - _codec.NextIn; - _codec.NextIn = p; - write = q; - return Flush(r); - } - } - } - - internal void Free() - { - Reset(null); - window = null; - hufts = null; - //ZFREE(z, s); - } - - internal void SetDictionary(byte[] d, int start, int n) - { - Array.Copy(d, start, window, 0, n); - read = write = n; - } - - // Returns true if inflate is currently at the end of a block generated - // by Z_SYNC_FLUSH or Z_FULL_FLUSH. - internal int SyncPoint() - { - return mode == LENS ? 1 : 0; - } - - // copy as much as possible from the sliding window to the output area - internal int Flush(int r) - { - int n; - int p; - int q; - - // local copies of source and destination pointers - p = _codec.NextOut; - q = read; - - // compute number of bytes to copy as far as end of window - n = (int)((q <= write ? write : end) - q); - if (n > _codec.AvailableBytesOut) - n = _codec.AvailableBytesOut; - if (n != 0 && r == ZlibConstants.Z_BUF_ERROR) - r = ZlibConstants.Z_OK; - - // update counters - _codec.AvailableBytesOut -= n; - _codec.TotalBytesOut += n; - - // update check information - if (checkfn != null) - _codec._Adler32 = check = Adler.Adler32(check, window, q, n); - - // copy as far as end of window - Array.Copy(window, q, _codec.OutputBuffer, p, n); - p += n; - q += n; - - // see if more to copy at beginning of window - if (q == end) - { - // wrap pointers - q = 0; - if (write == end) - write = 0; - - // compute bytes to copy - n = write - q; - if (n > _codec.AvailableBytesOut) - n = _codec.AvailableBytesOut; - if (n != 0 && r == ZlibConstants.Z_BUF_ERROR) - r = ZlibConstants.Z_OK; - - // update counters - _codec.AvailableBytesOut -= n; - _codec.TotalBytesOut += n; - - // update check information - if (checkfn != null) - _codec._Adler32 = check = Adler.Adler32(check, window, q, n); - - // copy - Array.Copy(window, q, _codec.OutputBuffer, p, n); - p += n; - q += n; - } - - // update pointers - _codec.NextOut = p; - read = q; - - // done - return r; - } - } - - sealed internal class InflateCodes - { - //UPGRADE_NOTE: Final was removed from the declaration of 'inflate_mask'. "ms-help://MS.VSCC.v80/dv_commoner/local/redirect.htm?index='!DefaultContextWindowIndex'&keyword='jlca1003'" - private static readonly int[] inflate_mask = new int[] { 0x00000000, 0x00000001, 0x00000003, 0x00000007, 0x0000000f, 0x0000001f, 0x0000003f, 0x0000007f, 0x000000ff, 0x000001ff, 0x000003ff, 0x000007ff, 0x00000fff, 0x00001fff, 0x00003fff, 0x00007fff, 0x0000ffff }; - - // waiting for "i:"=input, - // "o:"=output, - // "x:"=nothing - private const int START = 0; // x: set up for LEN - private const int LEN = 1; // i: get length/literal/eob next - private const int LENEXT = 2; // i: getting length extra (have base) - private const int DIST = 3; // i: get distance next - private const int DISTEXT = 4; // i: getting distance extra - private const int COPY = 5; // o: copying bytes in window, waiting for space - private const int LIT = 6; // o: got literal, waiting for output space - private const int WASH = 7; // o: got eob, possibly still output waiting - private const int END = 8; // x: got eob and all data flushed - private const int BADCODE = 9; // x: got error - - internal int mode; // current inflate_codes mode - - // mode dependent information - internal int len; - - internal int[] tree; // pointer into tree - internal int tree_index = 0; - internal int need; // bits needed - - internal int lit; - - // if EXT or COPY, where and how much - internal int get_Renamed; // bits to get for extra - internal int dist; // distance back to copy from - - internal byte lbits; // ltree bits decoded per branch - internal byte dbits; // dtree bits decoder per branch - internal int[] ltree; // literal/length/eob tree - internal int ltree_index; // literal/length/eob tree - internal int[] dtree; // distance tree - internal int dtree_index; // distance tree - - internal InflateCodes() - { - } - - internal void Init(int bl, int bd, int[] tl, int tl_index, int[] td, int td_index) - { - mode = START; - lbits = (byte)bl; - dbits = (byte)bd; - ltree = tl; - ltree_index = tl_index; - dtree = td; - dtree_index = td_index; - tree = null; - } - - internal int Process(InflateBlocks blocks, int r) - { - int j; // temporary storage - int tindex; // temporary pointer - int e; // extra bits or operation - int b = 0; // bit buffer - int k = 0; // bits in bit buffer - int p = 0; // input data pointer - int n; // bytes available there - int q; // output window write pointer - int m; // bytes to end of window or read pointer - int f; // pointer to copy strings from - - ZlibCodec z = blocks._codec; - - // copy input/output information to locals (UPDATE macro restores) - p = z.NextIn; - n = z.AvailableBytesIn; - b = blocks.bitb; - k = blocks.bitk; - q = blocks.write; m = q < blocks.read ? blocks.read - q - 1 : blocks.end - q; - - // process input and output based on current state - while (true) - { - switch (mode) - { - // waiting for "i:"=input, "o:"=output, "x:"=nothing - case START: // x: set up for LEN - if (m >= 258 && n >= 10) - { - - blocks.bitb = b; blocks.bitk = k; - z.AvailableBytesIn = n; - z.TotalBytesIn += p - z.NextIn; - z.NextIn = p; - blocks.write = q; - r = InflateFast(lbits, dbits, ltree, ltree_index, dtree, dtree_index, blocks, z); - - p = z.NextIn; - n = z.AvailableBytesIn; - b = blocks.bitb; - k = blocks.bitk; - q = blocks.write; m = q < blocks.read ? blocks.read - q - 1 : blocks.end - q; - - if (r != ZlibConstants.Z_OK) - { - mode = (r == ZlibConstants.Z_STREAM_END) ? WASH : BADCODE; - break; - } - } - need = lbits; - tree = ltree; - tree_index = ltree_index; - - mode = LEN; - goto case LEN; - - case LEN: // i: get length/literal/eob next - j = need; - - while (k < (j)) - { - if (n != 0) - r = ZlibConstants.Z_OK; - else - { - blocks.bitb = b; blocks.bitk = k; - z.AvailableBytesIn = n; - z.TotalBytesIn += p - z.NextIn; - z.NextIn = p; - blocks.write = q; - return blocks.Flush(r); - } - n--; - b |= (z.InputBuffer[p++] & 0xff) << k; - k += 8; - } - - tindex = (tree_index + (b & inflate_mask[j])) * 3; - - b = SharedUtils.URShift(b, (tree[tindex + 1])); - k -= (tree[tindex + 1]); - - e = tree[tindex]; - - if (e == 0) - { - // literal - lit = tree[tindex + 2]; - mode = LIT; - break; - } - if ((e & 16) != 0) - { - // length - get_Renamed = e & 15; - len = tree[tindex + 2]; - mode = LENEXT; - break; - } - if ((e & 64) == 0) - { - // next table - need = e; - tree_index = tindex / 3 + tree[tindex + 2]; - break; - } - if ((e & 32) != 0) - { - // end of block - mode = WASH; - break; - } - mode = BADCODE; // invalid code - z.Message = "invalid literal/length code"; - r = ZlibConstants.Z_DATA_ERROR; - - blocks.bitb = b; blocks.bitk = k; - z.AvailableBytesIn = n; - z.TotalBytesIn += p - z.NextIn; - z.NextIn = p; - blocks.write = q; - return blocks.Flush(r); - - - case LENEXT: // i: getting length extra (have base) - j = get_Renamed; - - while (k < (j)) - { - if (n != 0) - r = ZlibConstants.Z_OK; - else - { - - blocks.bitb = b; blocks.bitk = k; - z.AvailableBytesIn = n; z.TotalBytesIn += p - z.NextIn; z.NextIn = p; - blocks.write = q; - return blocks.Flush(r); - } - n--; b |= (z.InputBuffer[p++] & 0xff) << k; - k += 8; - } - - len += (b & inflate_mask[j]); - - b >>= j; - k -= j; - - need = dbits; - tree = dtree; - tree_index = dtree_index; - mode = DIST; - goto case DIST; - - case DIST: // i: get distance next - j = need; - - while (k < (j)) - { - if (n != 0) - r = ZlibConstants.Z_OK; - else - { - - blocks.bitb = b; blocks.bitk = k; - z.AvailableBytesIn = n; z.TotalBytesIn += p - z.NextIn; z.NextIn = p; - blocks.write = q; - return blocks.Flush(r); - } - n--; b |= (z.InputBuffer[p++] & 0xff) << k; - k += 8; - } - - tindex = (tree_index + (b & inflate_mask[j])) * 3; - - b >>= tree[tindex + 1]; - k -= tree[tindex + 1]; - - e = (tree[tindex]); - if ((e & 16) != 0) - { - // distance - get_Renamed = e & 15; - dist = tree[tindex + 2]; - mode = DISTEXT; - break; - } - if ((e & 64) == 0) - { - // next table - need = e; - tree_index = tindex / 3 + tree[tindex + 2]; - break; - } - mode = BADCODE; // invalid code - z.Message = "invalid distance code"; - r = ZlibConstants.Z_DATA_ERROR; - - blocks.bitb = b; blocks.bitk = k; - z.AvailableBytesIn = n; z.TotalBytesIn += p - z.NextIn; z.NextIn = p; - blocks.write = q; - return blocks.Flush(r); - - - case DISTEXT: // i: getting distance extra - j = get_Renamed; - - while (k < (j)) - { - if (n != 0) - r = ZlibConstants.Z_OK; - else - { - - blocks.bitb = b; blocks.bitk = k; - z.AvailableBytesIn = n; z.TotalBytesIn += p - z.NextIn; z.NextIn = p; - blocks.write = q; - return blocks.Flush(r); - } - n--; b |= (z.InputBuffer[p++] & 0xff) << k; - k += 8; - } - - dist += (b & inflate_mask[j]); - - b >>= j; - k -= j; - - mode = COPY; - goto case COPY; - - case COPY: // o: copying bytes in window, waiting for space - f = q - dist; - while (f < 0) - { - // modulo window size-"while" instead - f += blocks.end; // of "if" handles invalid distances - } - while (len != 0) - { - - if (m == 0) - { - if (q == blocks.end && blocks.read != 0) - { - q = 0; m = q < blocks.read ? blocks.read - q - 1 : blocks.end - q; - } - if (m == 0) - { - blocks.write = q; r = blocks.Flush(r); - q = blocks.write; m = q < blocks.read ? blocks.read - q - 1 : blocks.end - q; - - if (q == blocks.end && blocks.read != 0) - { - q = 0; m = q < blocks.read ? blocks.read - q - 1 : blocks.end - q; - } - - if (m == 0) - { - blocks.bitb = b; blocks.bitk = k; - z.AvailableBytesIn = n; - z.TotalBytesIn += p - z.NextIn; - z.NextIn = p; - blocks.write = q; - return blocks.Flush(r); - } - } - } - - blocks.window[q++] = blocks.window[f++]; m--; - - if (f == blocks.end) - f = 0; - len--; - } - mode = START; - break; - - case LIT: // o: got literal, waiting for output space - if (m == 0) - { - if (q == blocks.end && blocks.read != 0) - { - q = 0; m = q < blocks.read ? blocks.read - q - 1 : blocks.end - q; - } - if (m == 0) - { - blocks.write = q; r = blocks.Flush(r); - q = blocks.write; m = q < blocks.read ? blocks.read - q - 1 : blocks.end - q; - - if (q == blocks.end && blocks.read != 0) - { - q = 0; m = q < blocks.read ? blocks.read - q - 1 : blocks.end - q; - } - if (m == 0) - { - blocks.bitb = b; blocks.bitk = k; - z.AvailableBytesIn = n; z.TotalBytesIn += p - z.NextIn; z.NextIn = p; - blocks.write = q; - return blocks.Flush(r); - } - } - } - r = ZlibConstants.Z_OK; - - blocks.window[q++] = (byte)lit; m--; - - mode = START; - break; - - case WASH: // o: got eob, possibly more output - if (k > 7) - { - // return unused byte, if any - k -= 8; - n++; - p--; // can always return one - } - - blocks.write = q; r = blocks.Flush(r); - q = blocks.write; m = q < blocks.read ? blocks.read - q - 1 : blocks.end - q; - - if (blocks.read != blocks.write) - { - blocks.bitb = b; blocks.bitk = k; - z.AvailableBytesIn = n; z.TotalBytesIn += p - z.NextIn; z.NextIn = p; - blocks.write = q; - return blocks.Flush(r); - } - mode = END; - goto case END; - - case END: - r = ZlibConstants.Z_STREAM_END; - blocks.bitb = b; blocks.bitk = k; - z.AvailableBytesIn = n; z.TotalBytesIn += p - z.NextIn; z.NextIn = p; - blocks.write = q; - return blocks.Flush(r); - - - case BADCODE: // x: got error - - r = ZlibConstants.Z_DATA_ERROR; - - blocks.bitb = b; blocks.bitk = k; - z.AvailableBytesIn = n; z.TotalBytesIn += p - z.NextIn; z.NextIn = p; - blocks.write = q; - return blocks.Flush(r); - - - default: - r = ZlibConstants.Z_STREAM_ERROR; - - blocks.bitb = b; blocks.bitk = k; - z.AvailableBytesIn = n; z.TotalBytesIn += p - z.NextIn; z.NextIn = p; - blocks.write = q; - return blocks.Flush(r); - - } - } - } - - - // Called with number of bytes left to write in window at least 258 - // (the maximum string length) and number of input bytes available - // at least ten. The ten bytes are six bytes for the longest length/ - // distance pair plus four bytes for overloading the bit buffer. - - internal int InflateFast(int bl, int bd, int[] tl, int tl_index, int[] td, int td_index, InflateBlocks s, ZlibCodec z) - { - int t; // temporary pointer - int[] tp; // temporary pointer - int tp_index; // temporary pointer - int e; // extra bits or operation - int b; // bit buffer - int k; // bits in bit buffer - int p; // input data pointer - int n; // bytes available there - int q; // output window write pointer - int m; // bytes to end of window or read pointer - int ml; // mask for literal/length tree - int md; // mask for distance tree - int c; // bytes to copy - int d; // distance back to copy from - int r; // copy source pointer - - int tp_index_t_3; // (tp_index+t)*3 - - // load input, output, bit values - p = z.NextIn; n = z.AvailableBytesIn; b = s.bitb; k = s.bitk; - q = s.write; m = q < s.read ? s.read - q - 1 : s.end - q; - - // initialize masks - ml = inflate_mask[bl]; - md = inflate_mask[bd]; - - // do until not enough input or output space for fast loop - do - { - // assume called with m >= 258 && n >= 10 - // get literal/length code - while (k < (20)) - { - // max bits for literal/length code - n--; - b |= (z.InputBuffer[p++] & 0xff) << k; k += 8; - } - - t = b & ml; - tp = tl; - tp_index = tl_index; - tp_index_t_3 = (tp_index + t) * 3; - if ((e = tp[tp_index_t_3]) == 0) - { - b >>= (tp[tp_index_t_3 + 1]); k -= (tp[tp_index_t_3 + 1]); - - s.window[q++] = (byte)tp[tp_index_t_3 + 2]; - m--; - continue; - } - do - { - - b >>= (tp[tp_index_t_3 + 1]); k -= (tp[tp_index_t_3 + 1]); - - if ((e & 16) != 0) - { - e &= 15; - c = tp[tp_index_t_3 + 2] + ((int)b & inflate_mask[e]); - - b >>= e; k -= e; - - // decode distance base of block to copy - while (k < (15)) - { - // max bits for distance code - n--; - b |= (z.InputBuffer[p++] & 0xff) << k; k += 8; - } - - t = b & md; - tp = td; - tp_index = td_index; - tp_index_t_3 = (tp_index + t) * 3; - e = tp[tp_index_t_3]; - - do - { - - b >>= (tp[tp_index_t_3 + 1]); k -= (tp[tp_index_t_3 + 1]); - - if ((e & 16) != 0) - { - // get extra bits to add to distance base - e &= 15; - while (k < (e)) - { - // get extra bits (up to 13) - n--; - b |= (z.InputBuffer[p++] & 0xff) << k; k += 8; - } - - d = tp[tp_index_t_3 + 2] + (b & inflate_mask[e]); - - b >>= (e); k -= (e); - - // do the copy - m -= c; - if (q >= d) - { - // offset before dest - // just copy - r = q - d; - if (q - r > 0 && 2 > (q - r)) - { - s.window[q++] = s.window[r++]; // minimum count is three, - s.window[q++] = s.window[r++]; // so unroll loop a little - c -= 2; - } - else - { - Array.Copy(s.window, r, s.window, q, 2); - q += 2; r += 2; c -= 2; - } - } - else - { - // else offset after destination - r = q - d; - do - { - r += s.end; // force pointer in window - } - while (r < 0); // covers invalid distances - e = s.end - r; - if (c > e) - { - // if source crosses, - c -= e; // wrapped copy - if (q - r > 0 && e > (q - r)) - { - do - { - s.window[q++] = s.window[r++]; - } - while (--e != 0); - } - else - { - Array.Copy(s.window, r, s.window, q, e); - q += e; r += e; e = 0; - } - r = 0; // copy rest from start of window - } - } - - // copy all or what's left - if (q - r > 0 && c > (q - r)) - { - do - { - s.window[q++] = s.window[r++]; - } - while (--c != 0); - } - else - { - Array.Copy(s.window, r, s.window, q, c); - q += c; r += c; c = 0; - } - break; - } - else if ((e & 64) == 0) - { - t += tp[tp_index_t_3 + 2]; - t += (b & inflate_mask[e]); - tp_index_t_3 = (tp_index + t) * 3; - e = tp[tp_index_t_3]; - } - else - { - z.Message = "invalid distance code"; - - c = z.AvailableBytesIn - n; c = (k >> 3) < c ? k >> 3 : c; n += c; p -= c; k -= (c << 3); - - s.bitb = b; s.bitk = k; - z.AvailableBytesIn = n; z.TotalBytesIn += p - z.NextIn; z.NextIn = p; - s.write = q; - - return ZlibConstants.Z_DATA_ERROR; - } - } - while (true); - break; - } - - if ((e & 64) == 0) - { - t += tp[tp_index_t_3 + 2]; - t += (b & inflate_mask[e]); - tp_index_t_3 = (tp_index + t) * 3; - if ((e = tp[tp_index_t_3]) == 0) - { - - b >>= (tp[tp_index_t_3 + 1]); k -= (tp[tp_index_t_3 + 1]); - - s.window[q++] = (byte)tp[tp_index_t_3 + 2]; - m--; - break; - } - } - else if ((e & 32) != 0) - { - - c = z.AvailableBytesIn - n; c = (k >> 3) < c ? k >> 3 : c; n += c; p -= c; k -= (c << 3); - - s.bitb = b; s.bitk = k; - z.AvailableBytesIn = n; z.TotalBytesIn += p - z.NextIn; z.NextIn = p; - s.write = q; - - return ZlibConstants.Z_STREAM_END; - } - else - { - z.Message = "invalid literal/length code"; - - c = z.AvailableBytesIn - n; c = (k >> 3) < c ? k >> 3 : c; n += c; p -= c; k -= (c << 3); - - s.bitb = b; s.bitk = k; - z.AvailableBytesIn = n; z.TotalBytesIn += p - z.NextIn; z.NextIn = p; - s.write = q; - - return ZlibConstants.Z_DATA_ERROR; - } - } - while (true); - } - while (m >= 258 && n >= 10); - - // not enough input or output--restore pointers and return - c = z.AvailableBytesIn - n; c = (k >> 3) < c ? k >> 3 : c; n += c; p -= c; k -= (c << 3); - - s.bitb = b; s.bitk = k; - z.AvailableBytesIn = n; z.TotalBytesIn += p - z.NextIn; z.NextIn = p; - s.write = q; - - return ZlibConstants.Z_OK; - } - } - - internal sealed class InflateManager - { - // preset dictionary flag in zlib header - private const int PRESET_DICT = 0x20; - - private const int Z_DEFLATED = 8; - - private const int METHOD = 0; // waiting for method byte - private const int FLAG = 1; // waiting for flag byte - private const int DICT4 = 2; // four dictionary check bytes to go - private const int DICT3 = 3; // three dictionary check bytes to go - private const int DICT2 = 4; // two dictionary check bytes to go - private const int DICT1 = 5; // one dictionary check byte to go - private const int DICT0 = 6; // waiting for inflateSetDictionary - private const int BLOCKS = 7; // decompressing blocks - private const int CHECK4 = 8; // four check bytes to go - private const int CHECK3 = 9; // three check bytes to go - private const int CHECK2 = 10; // two check bytes to go - private const int CHECK1 = 11; // one check byte to go - private const int DONE = 12; // finished check, done - private const int BAD = 13; // got an error--stay here - - internal int mode; // current inflate mode - internal ZlibCodec _codec; // pointer back to this zlib stream - - // mode dependent information - internal int method; // if FLAGS, method byte - - // if CHECK, check values to compare - internal long[] was = new long[1]; // computed check value - internal long need; // stream check value - - // if BAD, inflateSync's marker bytes count - internal int marker; - - // mode independent information - //internal int nowrap; // flag for no wrapper - private bool _handleRfc1950HeaderBytes = true; - internal bool HandleRfc1950HeaderBytes - { - get { return _handleRfc1950HeaderBytes; } - set { _handleRfc1950HeaderBytes = value; } - } - internal int wbits; // log2(window size) (8..15, defaults to 15) - - internal InflateBlocks blocks; // current inflate_blocks state - - public InflateManager() { } - - public InflateManager(bool expectRfc1950HeaderBytes) - { - _handleRfc1950HeaderBytes = expectRfc1950HeaderBytes; - } - - internal int Reset() - { - _codec.TotalBytesIn = _codec.TotalBytesOut = 0; - _codec.Message = null; - mode = HandleRfc1950HeaderBytes ? METHOD : BLOCKS; - blocks.Reset(null); - return ZlibConstants.Z_OK; - } - - internal int End() - { - if (blocks != null) - blocks.Free(); - blocks = null; - return ZlibConstants.Z_OK; - } - - internal int Initialize(ZlibCodec codec, int w) - { - _codec = codec; - _codec.Message = null; - blocks = null; - - // handle undocumented nowrap option (no zlib header or check) - //nowrap = 0; - //if (w < 0) - //{ - // w = - w; - // nowrap = 1; - //} - - // set window size - if (w < 8 || w > 15) - { - End(); - throw new ZlibException("Bad window size."); - - //return ZlibConstants.Z_STREAM_ERROR; - } - wbits = w; - - blocks = new InflateBlocks(codec, - HandleRfc1950HeaderBytes ? this : null, - 1 << w); - - // reset state - Reset(); - return ZlibConstants.Z_OK; - } - - internal int Inflate(FlushType flush) - { - int r; - int b; - int f = (int)flush; - - if (_codec.InputBuffer == null) - throw new ZlibException("InputBuffer is null. "); - - f = (f == (int)FlushType.Finish) - ? ZlibConstants.Z_BUF_ERROR - : ZlibConstants.Z_OK; - r = ZlibConstants.Z_BUF_ERROR; - while (true) - { - switch (mode) - { - case METHOD: - if (_codec.AvailableBytesIn == 0) - return r; - - r = f; - - _codec.AvailableBytesIn--; _codec.TotalBytesIn++; - - if (((method = _codec.InputBuffer[_codec.NextIn++]) & 0xf) != Z_DEFLATED) - { - mode = BAD; - _codec.Message = String.Format("unknown compression method (0x{0:X2})", method); - marker = 5; // can't try inflateSync - break; - } - if ((method >> 4) + 8 > wbits) - { - mode = BAD; - _codec.Message = String.Format("invalid window size ({0})", (method >> 4) + 8); - marker = 5; // can't try inflateSync - break; - } - mode = FLAG; - goto case FLAG; - - case FLAG: - - if (_codec.AvailableBytesIn == 0) return r; - - r = f; - - _codec.AvailableBytesIn--; _codec.TotalBytesIn++; - b = (_codec.InputBuffer[_codec.NextIn++]) & 0xff; - - if ((((method << 8) + b) % 31) != 0) - { - mode = BAD; - _codec.Message = "incorrect header check"; - marker = 5; // can't try inflateSync - break; - } - - if ((b & PRESET_DICT) == 0) - { - mode = BLOCKS; - break; - } - mode = DICT4; - goto case DICT4; - - case DICT4: - - if (_codec.AvailableBytesIn == 0) return r; - - r = f; - - _codec.AvailableBytesIn--; _codec.TotalBytesIn++; - need = ((_codec.InputBuffer[_codec.NextIn++] & 0xff) << 24) & unchecked((int)0xff000000L); - mode = DICT3; - goto case DICT3; - - case DICT3: - - if (_codec.AvailableBytesIn == 0) return r; - - r = f; - - _codec.AvailableBytesIn--; _codec.TotalBytesIn++; - need += (((_codec.InputBuffer[_codec.NextIn++] & 0xff) << 16) & 0xff0000L); - mode = DICT2; - goto case DICT2; - - case DICT2: - - if (_codec.AvailableBytesIn == 0) return r; - - r = f; - - _codec.AvailableBytesIn--; _codec.TotalBytesIn++; - need += (((_codec.InputBuffer[_codec.NextIn++] & 0xff) << 8) & 0xff00L); - mode = DICT1; - goto case DICT1; - - case DICT1: - - if (_codec.AvailableBytesIn == 0) return r; - - r = f; - - _codec.AvailableBytesIn--; _codec.TotalBytesIn++; - need += (_codec.InputBuffer[_codec.NextIn++] & 0xffL); - _codec._Adler32 = need; - mode = DICT0; - return ZlibConstants.Z_NEED_DICT; - - case DICT0: - mode = BAD; - _codec.Message = "need dictionary"; - marker = 0; // can try inflateSync - return ZlibConstants.Z_STREAM_ERROR; - - case BLOCKS: - r = blocks.Process(r); - if (r == ZlibConstants.Z_DATA_ERROR) - { - mode = BAD; - marker = 0; // can try inflateSync - break; - } - if (r == ZlibConstants.Z_OK) r = f; - - if (r != ZlibConstants.Z_STREAM_END) return r; - - r = f; - blocks.Reset(was); - if (!HandleRfc1950HeaderBytes) - { - mode = DONE; - break; - } - mode = CHECK4; - goto case CHECK4; - - case CHECK4: - - if (_codec.AvailableBytesIn == 0) return r; - - r = f; - - _codec.AvailableBytesIn--; _codec.TotalBytesIn++; - need = ((_codec.InputBuffer[_codec.NextIn++] & 0xff) << 24) & unchecked((int)0xff000000L); - mode = CHECK3; - goto case CHECK3; - - case CHECK3: - if (_codec.AvailableBytesIn == 0) return r; - r = f; - _codec.AvailableBytesIn--; _codec.TotalBytesIn++; - need += (((_codec.InputBuffer[_codec.NextIn++] & 0xff) << 16) & 0xff0000L); - mode = CHECK2; - goto case CHECK2; - - case CHECK2: - if (_codec.AvailableBytesIn == 0) return r; - r = f; - - _codec.AvailableBytesIn--; - _codec.TotalBytesIn++; - need += (((_codec.InputBuffer[_codec.NextIn++] & 0xff) << 8) & 0xff00L); - mode = CHECK1; - goto case CHECK1; - - case CHECK1: - - if (_codec.AvailableBytesIn == 0) return r; - - r = f; - - _codec.AvailableBytesIn--; _codec.TotalBytesIn++; - need += (_codec.InputBuffer[_codec.NextIn++] & 0xffL); - unchecked - { - if (((int)(was[0])) != ((int)(need))) - { - mode = BAD; - _codec.Message = "incorrect data check"; - marker = 5; // can't try inflateSync - break; - } - } - mode = DONE; - goto case DONE; - - case DONE: - return ZlibConstants.Z_STREAM_END; - - case BAD: - throw new ZlibException(String.Format("Bad state ({0})", _codec.Message)); - //return ZlibConstants.Z_DATA_ERROR; - - default: - throw new ZlibException("Stream error."); - //return ZlibConstants.Z_STREAM_ERROR; - - } - } - } - - - - internal int SetDictionary(byte[] dictionary) - { - int index = 0; - int length = dictionary.Length; - if (mode != DICT0) - throw new ZlibException("Stream error."); - - if (Adler.Adler32(1L, dictionary, 0, dictionary.Length) != _codec._Adler32) - { - return ZlibConstants.Z_DATA_ERROR; - } - - _codec._Adler32 = Adler.Adler32(0, null, 0, 0); - - if (length >= (1 << wbits)) - { - length = (1 << wbits) - 1; - index = dictionary.Length - length; - } - blocks.SetDictionary(dictionary, index, length); - mode = BLOCKS; - return ZlibConstants.Z_OK; - } - - private static byte[] mark = new byte[] { 0, 0, 0xff, 0xff }; - - internal int Sync() - { - int n; // number of bytes to look at - int p; // pointer to bytes - int m; // number of marker bytes found in a row - long r, w; // temporaries to save total_in and total_out - - // set up - if (mode != BAD) - { - mode = BAD; - marker = 0; - } - if ((n = _codec.AvailableBytesIn) == 0) - return ZlibConstants.Z_BUF_ERROR; - p = _codec.NextIn; - m = marker; - - // search - while (n != 0 && m < 4) - { - if (_codec.InputBuffer[p] == mark[m]) - { - m++; - } - else if (_codec.InputBuffer[p] != 0) - { - m = 0; - } - else - { - m = 4 - m; - } - p++; n--; - } - - // restore - _codec.TotalBytesIn += p - _codec.NextIn; - _codec.NextIn = p; - _codec.AvailableBytesIn = n; - marker = m; - - // return no joy or set up to restart on a new block - if (m != 4) - { - return ZlibConstants.Z_DATA_ERROR; - } - r = _codec.TotalBytesIn; - w = _codec.TotalBytesOut; - Reset(); - _codec.TotalBytesIn = r; - _codec.TotalBytesOut = w; - mode = BLOCKS; - return ZlibConstants.Z_OK; - } - - // Returns true if inflate is currently at the end of a block generated - // by Z_SYNC_FLUSH or Z_FULL_FLUSH. This function is used by one PPP - // implementation to provide an additional safety check. PPP uses Z_SYNC_FLUSH - // but removes the length bytes of the resulting empty stored block. When - // decompressing, PPP checks that at the end of input packet, inflate is - // waiting for these length bytes. - internal int SyncPoint(ZlibCodec z) - { - return blocks.SyncPoint(); - } - } -} - -#endif \ No newline at end of file diff --git a/RestSharp/Compression/ZLib/ZLib.cs b/RestSharp/Compression/ZLib/ZLib.cs deleted file mode 100644 index 580dde439..000000000 --- a/RestSharp/Compression/ZLib/ZLib.cs +++ /dev/null @@ -1,295 +0,0 @@ -// Zlib.cs -// ------------------------------------------------------------------ -// -// Copyright (c) 2009 Dino Chiesa and Microsoft Corporation. -// All rights reserved. -// -// This code module is part of DotNetZip, a zipfile class library. -// -// ------------------------------------------------------------------ -// -// This code is licensed under the Microsoft Public License. -// See the file License.txt for the license details. -// More info on: http://dotnetzip.codeplex.com -// -// ------------------------------------------------------------------ -// -// last saved (in emacs): -// Time-stamp: <2009-August-14 09:51:43> -// -// ------------------------------------------------------------------ -// -// This module defines classes for ZLIB compression and -// decompression. This code is derived from the jzlib implementation of -// zlib, but significantly modified. The object model is not the same, -// and many of the behaviors are new or different. Nonetheless, in -// keeping with the license for jzlib, the copyright to that code is -// included below. -// -// ------------------------------------------------------------------ -// -// Copyright (c) 2000,2001,2002,2003 ymnk, JCraft,Inc. All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright -// notice, this list of conditions and the following disclaimer in -// the documentation and/or other materials provided with the distribution. -// -// 3. The names of the authors may not be used to endorse or promote products -// derived from this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, -// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND -// FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT, -// INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, -// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, -// OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, -// EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -// -// ----------------------------------------------------------------------- -// -// This program is based on zlib-1.1.3; credit to authors -// Jean-loup Gailly(jloup@gzip.org) and Mark Adler(madler@alumni.caltech.edu) -// and contributors of zlib. -// -// ----------------------------------------------------------------------- - -#if WINDOWS_PHONE - -using System; -using Interop = System.Runtime.InteropServices; - -namespace RestSharp.Compression.ZLib -{ - /// - /// A general purpose exception class for exceptions in the Zlib library. - /// - internal class ZlibException : System.Exception - { - /// - /// The ZlibException class captures exception information generated - /// by the Zlib library. - /// - public ZlibException() - : base() - { - } - - /// - /// This ctor collects a message attached to the exception. - /// - /// - public ZlibException(System.String s) - : base(s) - { - } - } - - - internal class SharedUtils - { - /// - /// Performs an unsigned bitwise right shift with the specified number - /// - /// Number to operate on - /// Ammount of bits to shift - /// The resulting number from the shift operation - public static int URShift(int number, int bits) - { - return (int)((uint)number >> bits); - } - - /// - /// Performs an unsigned bitwise right shift with the specified number - /// - /// Number to operate on - /// Ammount of bits to shift - /// The resulting number from the shift operation - public static long URShift(long number, int bits) - { - return (long)((UInt64)number >> bits); - } - - -#if NOTUSED - - /// - /// Performs an unsigned bitwise right shift with the specified number - /// - /// Number to operate on - /// Ammount of bits to shift - /// The resulting number from the shift operation - public static int URShift(int number, long bits) - { - return URShift(number, (int)bits); - } - - /// - /// Performs an unsigned bitwise right shift with the specified number - /// - /// Number to operate on - /// Ammount of bits to shift - /// The resulting number from the shift operation - public static long URShift(long number, long bits) - { - return URShift(number, (int)bits); - } - -#endif - -#if POINTLESS - /*******************************/ - /// Reads a number of characters from the current source Stream and writes the data to the target array at the specified index. - /// The source Stream to read from. - /// Contains the array of characteres read from the source Stream. - /// The starting index of the target array. - /// The maximum number of characters to read from the source Stream. - /// The number of characters read. The number will be less than or equal to count depending on the data available in the source Stream. Returns -1 if the end of the stream is reached. - public static System.Int32 ReadInput(System.IO.Stream sourceStream, byte[] target, int start, int count) - { - // Returns 0 bytes if not enough space in target - if (target.Length == 0) - return 0; - - if (count == 0) return 0; - - // why double-buffer? - //byte[] receiver = new byte[target.Length]; - int bytesRead = sourceStream.Read(target, start, count); - - // Returns -1 if EOF - //if (bytesRead == 0) - // return -1; - - //for (int i = start; i < start + bytesRead; i++) - // target[i] = (byte)receiver[i]; - - return bytesRead; - } -#endif - - - /// Reads a number of characters from the current source TextReader and writes the data to the target array at the specified index. - /// The source TextReader to read from - /// Contains the array of characteres read from the source TextReader. - /// The starting index of the target array. - /// The maximum number of characters to read from the source TextReader. - /// The number of characters read. The number will be less than or equal to count depending on the data available in the source TextReader. Returns -1 if the end of the stream is reached. - public static System.Int32 ReadInput(System.IO.TextReader sourceTextReader, byte[] target, int start, int count) - { - // Returns 0 bytes if not enough space in target - if (target.Length == 0) return 0; - - char[] charArray = new char[target.Length]; - int bytesRead = sourceTextReader.Read(charArray, start, count); - - // Returns -1 if EOF - if (bytesRead == 0) return -1; - - for (int index = start; index < start + bytesRead; index++) - target[index] = (byte)charArray[index]; - - return bytesRead; - } - - - internal static byte[] ToByteArray(System.String sourceString) - { - return System.Text.UTF8Encoding.UTF8.GetBytes(sourceString); - } - - - internal static char[] ToCharArray(byte[] byteArray) - { - return System.Text.UTF8Encoding.UTF8.GetChars(byteArray); - } - - } - - /// - /// Computes an Adler-32 checksum. - /// - /// - /// The Adler checksum is similar to a CRC checksum, but faster to compute, though less - /// reliable. It is used in producing RFC1950 compressed streams. The Adler checksum - /// is a required part of the "ZLIB" standard. Applications will almost never need to - /// use this class directly. - /// - internal sealed class Adler - { - // largest prime smaller than 65536 - private static int BASE = 65521; - // NMAX is the largest n such that 255n(n+1)/2 + (n+1)(BASE-1) <= 2^32-1 - private static int NMAX = 5552; - - static internal long Adler32(long adler, byte[] buf, int index, int len) - { - if (buf == null) - { - return 1L; - } - - long s1 = adler & 0xffff; - long s2 = (adler >> 16) & 0xffff; - int k; - - while (len > 0) - { - k = len < NMAX ? len : NMAX; - len -= k; - while (k >= 16) - { - s1 += (buf[index++] & 0xff); s2 += s1; - s1 += (buf[index++] & 0xff); s2 += s1; - s1 += (buf[index++] & 0xff); s2 += s1; - s1 += (buf[index++] & 0xff); s2 += s1; - s1 += (buf[index++] & 0xff); s2 += s1; - s1 += (buf[index++] & 0xff); s2 += s1; - s1 += (buf[index++] & 0xff); s2 += s1; - s1 += (buf[index++] & 0xff); s2 += s1; - s1 += (buf[index++] & 0xff); s2 += s1; - s1 += (buf[index++] & 0xff); s2 += s1; - s1 += (buf[index++] & 0xff); s2 += s1; - s1 += (buf[index++] & 0xff); s2 += s1; - s1 += (buf[index++] & 0xff); s2 += s1; - s1 += (buf[index++] & 0xff); s2 += s1; - s1 += (buf[index++] & 0xff); s2 += s1; - s1 += (buf[index++] & 0xff); s2 += s1; - k -= 16; - } - if (k != 0) - { - do - { - s1 += (buf[index++] & 0xff); s2 += s1; - } - while (--k != 0); - } - s1 %= BASE; - s2 %= BASE; - } - return (s2 << 16) | s1; - } - - /* - private java.util.zip.Adler32 adler=new java.util.zip.Adler32(); - long adler32(long value, byte[] buf, int index, int len){ - if(value==1) {adler.reset();} - if(buf==null) {adler.reset();} - else{adler.update(buf, index, len);} - return adler.getValue(); - } - */ - } - -} - -#endif \ No newline at end of file diff --git a/RestSharp/Compression/ZLib/ZLibCodec.cs b/RestSharp/Compression/ZLib/ZLibCodec.cs deleted file mode 100644 index 1f40d1acd..000000000 --- a/RestSharp/Compression/ZLib/ZLibCodec.cs +++ /dev/null @@ -1,358 +0,0 @@ -// ZlibCodec.cs -// ------------------------------------------------------------------ -// -// Copyright (c) 2009 Dino Chiesa and Microsoft Corporation. -// All rights reserved. -// -// This code module is part of DotNetZip, a zipfile class library. -// -// ------------------------------------------------------------------ -// -// This code is licensed under the Microsoft Public License. -// See the file License.txt for the license details. -// More info on: http://dotnetzip.codeplex.com -// -// ------------------------------------------------------------------ -// -// last saved (in emacs): -// Time-stamp: <2009-August-18 21:02:55> -// -// ------------------------------------------------------------------ -// -// This module defines a Codec for ZLIB compression and -// decompression. This code extends code that was based the jzlib -// implementation of zlib, but this code is completely novel. The codec -// class is new, and encapsulates some behaviors that are new, and some -// that were present in other classes in the jzlib code base. In -// keeping with the license for jzlib, the copyright to the jzlib code -// is included below. -// -// ------------------------------------------------------------------ -// -// Copyright (c) 2000,2001,2002,2003 ymnk, JCraft,Inc. All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright -// notice, this list of conditions and the following disclaimer in -// the documentation and/or other materials provided with the distribution. -// -// 3. The names of the authors may not be used to endorse or promote products -// derived from this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, -// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND -// FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT, -// INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, -// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, -// OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, -// EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -// -// ----------------------------------------------------------------------- -// -// This program is based on zlib-1.1.3; credit to authors -// Jean-loup Gailly(jloup@gzip.org) and Mark Adler(madler@alumni.caltech.edu) -// and contributors of zlib. -// -// ----------------------------------------------------------------------- - -#if WINDOWS_PHONE - -using System; -using Interop = System.Runtime.InteropServices; - -namespace RestSharp.Compression.ZLib -{ - /// - /// Encoder and Decoder for ZLIB and DEFLATE (IETF RFC1950 and RFC1951). - /// - /// - /// - /// This class compresses and decompresses data according to the Deflate algorithm - /// and optionally, the ZLIB format, as documented in RFC 1950 - ZLIB and RFC 1951 - DEFLATE. - /// - sealed internal class ZlibCodec - { - /// - /// The buffer from which data is taken. - /// - public byte[] InputBuffer; - - /// - /// An index into the InputBuffer array, indicating where to start reading. - /// - public int NextIn; - - /// - /// The number of bytes available in the InputBuffer, starting at NextIn. - /// - /// - /// Generally you should set this to InputBuffer.Length before the first Inflate() or Deflate() call. - /// The class will update this number as calls to Inflate/Deflate are made. - /// - public int AvailableBytesIn; - - /// - /// Total number of bytes read so far, through all calls to Inflate()/Deflate(). - /// - public long TotalBytesIn; - - /// - /// Buffer to store output data. - /// - public byte[] OutputBuffer; - - /// - /// An index into the OutputBuffer array, indicating where to start writing. - /// - public int NextOut; - - /// - /// The number of bytes available in the OutputBuffer, starting at NextOut. - /// - /// - /// Generally you should set this to OutputBuffer.Length before the first Inflate() or Deflate() call. - /// The class will update this number as calls to Inflate/Deflate are made. - /// - public int AvailableBytesOut; - - /// - /// Total number of bytes written to the output so far, through all calls to Inflate()/Deflate(). - /// - public long TotalBytesOut; - - /// - /// used for diagnostics, when something goes wrong! - /// - public System.String Message; - - internal InflateManager istate; - - internal long _Adler32; - - /// - /// The number of Window Bits to use. - /// - /// - /// This gauges the size of the sliding window, and hence the - /// compression effectiveness as well as memory consumption. It's best to just leave this - /// setting alone if you don't know what it is. The maximum value is 15 bits, which implies - /// a 32k window. - /// - public int WindowBits = ZlibConstants.WindowBitsDefault; - - /// - /// The Adler32 checksum on the data transferred through the codec so far. You probably don't need to look at this. - /// - public long Adler32 { get { return _Adler32; } } - - - /// - /// Create a ZlibCodec that decompresses. - /// - public ZlibCodec() - { - int rc = InitializeInflate(); - if (rc != ZlibConstants.Z_OK) throw new ZlibException("Cannot initialize for inflate."); - } - - /// - /// Initialize the inflation state. - /// - /// - /// It is not necessary to call this before using the ZlibCodec to inflate data; - /// It is implicitly called when you call the constructor. - /// - /// Z_OK if everything goes well. - public int InitializeInflate() - { - return InitializeInflate(this.WindowBits); - } - - /// - /// Initialize the inflation state with an explicit flag to - /// govern the handling of RFC1950 header bytes. - /// - /// - /// - /// By default, the ZLIB header defined in RFC 1950 is expected. If - /// you want to read a zlib stream you should specify true for - /// expectRfc1950Header. If you have a deflate stream, you will want to specify - /// false. It is only necessary to invoke this initializer explicitly if you - /// want to specify false. - /// - /// - /// whether to expect an RFC1950 header byte - /// pair when reading the stream of data to be inflated. - /// - /// Z_OK if everything goes well. - public int InitializeInflate(bool expectRfc1950Header) - { - return InitializeInflate(this.WindowBits, expectRfc1950Header); - } - - /// - /// Initialize the ZlibCodec for inflation, with the specified number of window bits. - /// - /// The number of window bits to use. If you need to ask what that is, - /// then you shouldn't be calling this initializer. - /// Z_OK if all goes well. - public int InitializeInflate(int windowBits) - { - this.WindowBits = windowBits; - return InitializeInflate(windowBits, true); - } - - /// - /// Initialize the inflation state with an explicit flag to govern the handling of - /// RFC1950 header bytes. - /// - /// - /// - /// If you want to read a zlib stream you should specify true for - /// expectRfc1950Header. In this case, the library will expect to find a ZLIB - /// header, as defined in RFC - /// 1950, in the compressed stream. If you will be reading a DEFLATE or - /// GZIP stream, which does not have such a header, you will want to specify - /// false. - /// - /// - /// whether to expect an RFC1950 header byte pair when reading - /// the stream of data to be inflated. - /// The number of window bits to use. If you need to ask what that is, - /// then you shouldn't be calling this initializer. - /// Z_OK if everything goes well. - public int InitializeInflate(int windowBits, bool expectRfc1950Header) - { - this.WindowBits = windowBits; - //if (dstate != null) throw new ZlibException("You may not call InitializeInflate() after calling InitializeDeflate()."); - istate = new InflateManager(expectRfc1950Header); - return istate.Initialize(this, windowBits); - } - - /// - /// Inflate the data in the InputBuffer, placing the result in the OutputBuffer. - /// - /// - /// You must have set InputBuffer and OutputBuffer, NextIn and NextOut, and AvailableBytesIn and - /// AvailableBytesOut before calling this method. - /// - /// - /// - /// private void InflateBuffer() - /// { - /// int bufferSize = 1024; - /// byte[] buffer = new byte[bufferSize]; - /// ZlibCodec decompressor = new ZlibCodec(); - /// - /// Console.WriteLine("\n============================================"); - /// Console.WriteLine("Size of Buffer to Inflate: {0} bytes.", CompressedBytes.Length); - /// MemoryStream ms = new MemoryStream(DecompressedBytes); - /// - /// int rc = decompressor.InitializeInflate(); - /// - /// decompressor.InputBuffer = CompressedBytes; - /// decompressor.NextIn = 0; - /// decompressor.AvailableBytesIn = CompressedBytes.Length; - /// - /// decompressor.OutputBuffer = buffer; - /// - /// // pass 1: inflate - /// do - /// { - /// decompressor.NextOut = 0; - /// decompressor.AvailableBytesOut = buffer.Length; - /// rc = decompressor.Inflate(ZlibConstants.Z_NO_FLUSH); - /// - /// if (rc != ZlibConstants.Z_OK && rc != ZlibConstants.Z_STREAM_END) - /// throw new Exception("inflating: " + decompressor.Message); - /// - /// ms.Write(decompressor.OutputBuffer, 0, buffer.Length - decompressor.AvailableBytesOut); - /// } - /// while (decompressor.AvailableBytesIn > 0 || decompressor.AvailableBytesOut == 0); - /// - /// // pass 2: finish and flush - /// do - /// { - /// decompressor.NextOut = 0; - /// decompressor.AvailableBytesOut = buffer.Length; - /// rc = decompressor.Inflate(ZlibConstants.Z_FINISH); - /// - /// if (rc != ZlibConstants.Z_STREAM_END && rc != ZlibConstants.Z_OK) - /// throw new Exception("inflating: " + decompressor.Message); - /// - /// if (buffer.Length - decompressor.AvailableBytesOut > 0) - /// ms.Write(buffer, 0, buffer.Length - decompressor.AvailableBytesOut); - /// } - /// while (decompressor.AvailableBytesIn > 0 || decompressor.AvailableBytesOut == 0); - /// - /// decompressor.EndInflate(); - /// } - /// - /// - /// - /// The flush to use when inflating. - /// Z_OK if everything goes well. - public int Inflate(FlushType flush) - { - if (istate == null) - throw new ZlibException("No Inflate State!"); - return istate.Inflate(flush); - } - - - /// - /// Ends an inflation session. - /// - /// - /// Call this after successively calling Inflate(). This will cause all buffers to be flushed. - /// After calling this you cannot call Inflate() without a intervening call to one of the - /// InitializeInflate() overloads. - /// - /// Z_OK if everything goes well. - public int EndInflate() - { - if (istate == null) - throw new ZlibException("No Inflate State!"); - int ret = istate.End(); - istate = null; - return ret; - } - - /// - /// I don't know what this does! - /// - /// Z_OK if everything goes well. - public int SyncInflate() - { - if (istate == null) - throw new ZlibException("No Inflate State!"); - return istate.Sync(); - } - - /// - /// Set the dictionary to be used for either Inflation or Deflation. - /// - /// The dictionary bytes to use. - /// Z_OK if all goes well. - public int SetDictionary(byte[] dictionary) - { - if (istate != null) - return istate.SetDictionary(dictionary); - - throw new ZlibException("No Inflate state!"); - } - } -} - -#endif \ No newline at end of file diff --git a/RestSharp/Compression/ZLib/ZLibConstants.cs b/RestSharp/Compression/ZLib/ZLibConstants.cs deleted file mode 100644 index 905c21b24..000000000 --- a/RestSharp/Compression/ZLib/ZLibConstants.cs +++ /dev/null @@ -1,127 +0,0 @@ -// ZlibConstants.cs -// ------------------------------------------------------------------ -// -// Copyright (c) 2009 Dino Chiesa and Microsoft Corporation. -// All rights reserved. -// -// This code module is part of DotNetZip, a zipfile class library. -// -// ------------------------------------------------------------------ -// -// This code is licensed under the Microsoft Public License. -// See the file License.txt for the license details. -// More info on: http://dotnetzip.codeplex.com -// -// ------------------------------------------------------------------ -// -// last saved (in emacs): -// Time-stamp: <2009-May-31 09:35:39> -// -// ------------------------------------------------------------------ -// -// This module defines constants used by the zlib class library. This -// code is derived from the jzlib implementation of zlib, but -// significantly modified. In keeping with the license for jzlib, the -// copyright to that code is included here. -// -// ------------------------------------------------------------------ -// -// Copyright (c) 2000,2001,2002,2003 ymnk, JCraft,Inc. All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright -// notice, this list of conditions and the following disclaimer in -// the documentation and/or other materials provided with the distribution. -// -// 3. The names of the authors may not be used to endorse or promote products -// derived from this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, -// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND -// FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT, -// INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, -// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, -// OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, -// EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -// -// ----------------------------------------------------------------------- -// -// This program is based on zlib-1.1.3; credit to authors -// Jean-loup Gailly(jloup@gzip.org) and Mark Adler(madler@alumni.caltech.edu) -// and contributors of zlib. -// -// ----------------------------------------------------------------------- - -#if WINDOWS_PHONE - -using System; - -namespace RestSharp.Compression.ZLib -{ - /// - /// A bunch of constants used in the Zlib interface. - /// - internal static class ZlibConstants - { - /// - /// The maximum number of window bits for the Deflate algorithm. - /// - public const int WindowBitsMax = 15; // 32K LZ77 window - - /// - /// The default number of window bits for the Deflate algorithm. - /// - public const int WindowBitsDefault = WindowBitsMax; - - /// - /// indicates everything is A-OK - /// - public const int Z_OK = 0; - - /// - /// Indicates that the last operation reached the end of the stream. - /// - public const int Z_STREAM_END = 1; - - /// - /// The operation ended in need of a dictionary. - /// - public const int Z_NEED_DICT = 2; - - /// - /// There was an error with the stream - not enough data, not open and readable, etc. - /// - public const int Z_STREAM_ERROR = -2; - - /// - /// There was an error with the data - not enough data, bad data, etc. - /// - public const int Z_DATA_ERROR = -3; - - /// - /// There was an error with the working buffer. - /// - public const int Z_BUF_ERROR = -5; - - /// - /// The size of the working buffer used in the ZlibCodec class. Defaults to 8192 bytes. - /// - public const int WorkingBufferSizeDefault = 8192; // 8192; // 0x8000; // 16384; // 1024; - - /// - /// The minimum size of the working buffer used in the ZlibCodec class. Currently it is 128 bytes. - /// - public const int WorkingBufferSizeMin = 128; - } - -} - -#endif \ No newline at end of file diff --git a/RestSharp/Compression/ZLib/ZLibStream.cs b/RestSharp/Compression/ZLib/ZLibStream.cs deleted file mode 100644 index f2f46f9e0..000000000 --- a/RestSharp/Compression/ZLib/ZLibStream.cs +++ /dev/null @@ -1,875 +0,0 @@ -// ZlibStream.cs -// ------------------------------------------------------------------ -// -// Copyright (c) 2009 Dino Chiesa and Microsoft Corporation. -// All rights reserved. -// -// This code module is part of DotNetZip, a zipfile class library. -// -// ------------------------------------------------------------------ -// -// This code is licensed under the Microsoft Public License. -// See the file License.txt for the license details. -// More info on: http://dotnetzip.codeplex.com -// -// ------------------------------------------------------------------ -// -// last saved (in emacs): -// Time-stamp: <2009-August-19 18:39:59> -// -// ------------------------------------------------------------------ -// -// This module defines the ZlibStream class, which is similar in idea to -// the System.IO.Compression.DeflateStream and -// System.IO.Compression.GZipStream classes in the .NET BCL. -// -// ------------------------------------------------------------------ - -#if WINDOWS_PHONE - -using System; -using System.IO; - -namespace RestSharp.Compression.ZLib -{ - - /// - /// Represents a Zlib stream for compression or decompression. - /// - /// - /// - /// - /// The ZlibStream is a Decorator on a . It adds ZLIB compression or decompression to any - /// stream. - /// - /// - /// Using this stream, applications can compress or decompress data via - /// stream Read and Write operations. Either compresssion or - /// decompression can occur through either reading or writing. The compression - /// format used is ZLIB, which is documented in IETF RFC 1950, "ZLIB Compressed - /// Data Format Specification version 3.3". This implementation of ZLIB always uses - /// DEFLATE as the compression method. (see IETF RFC 1951, "DEFLATE - /// Compressed Data Format Specification version 1.3.") - /// - /// - /// The ZLIB format allows for varying compression methods, window sizes, and dictionaries. - /// This implementation always uses the DEFLATE compression method, a preset dictionary, - /// and 15 window bits by default. - /// - /// - /// - /// This class is similar to , except that it adds the - /// RFC1950 header and trailer bytes to a compressed stream when compressing, or expects - /// the RFC1950 header and trailer bytes when decompressing. It is also similar to the - /// . - /// - /// - /// - /// - internal class ZlibStream : System.IO.Stream - { - internal ZlibBaseStream _baseStream; - bool _disposed; - - public ZlibStream(System.IO.Stream stream) - { - _baseStream = new ZlibBaseStream(stream, ZlibStreamFlavor.ZLIB, false); - } - - #region Zlib properties - - /// - /// This property sets the flush behavior on the stream. - /// Sorry, though, not sure exactly how to describe all the various settings. - /// - virtual public FlushType FlushMode - { - get { return (this._baseStream._flushMode); } - set - { - if (_disposed) throw new ObjectDisposedException("ZlibStream"); - this._baseStream._flushMode = value; - } - } - - /// - /// The size of the working buffer for the compression codec. - /// - /// - /// - /// - /// The working buffer is used for all stream operations. The default size is 1024 bytes. - /// The minimum size is 128 bytes. You may get better performance with a larger buffer. - /// Then again, you might not. You would have to test it. - /// - /// - /// - /// Set this before the first call to Read() or Write() on the stream. If you try to set it - /// afterwards, it will throw. - /// - /// - public int BufferSize - { - get - { - return this._baseStream._bufferSize; - } - set - { - if (_disposed) throw new ObjectDisposedException("ZlibStream"); - if (this._baseStream._workingBuffer != null) - throw new ZlibException("The working buffer is already set."); - if (value < ZlibConstants.WorkingBufferSizeMin) - throw new ZlibException(String.Format("Don't be silly. {0} bytes?? Use a bigger buffer.", value)); - this._baseStream._bufferSize = value; - } - } - - /// Returns the total number of bytes input so far. - virtual public long TotalIn - { - get { return this._baseStream._z.TotalBytesIn; } - } - - /// Returns the total number of bytes output so far. - virtual public long TotalOut - { - get { return this._baseStream._z.TotalBytesOut; } - } - - #endregion - - #region System.IO.Stream methods - - /// - /// Dispose the stream. - /// - /// - /// This may or may not result in a Close() call on the captive stream. - /// See the constructors that have a leaveOpen parameter for more information. - /// - protected override void Dispose(bool disposing) - { - try - { - if (!_disposed) - { - if (disposing && (this._baseStream != null)) - this._baseStream.Close(); - _disposed = true; - } - } - finally - { - base.Dispose(disposing); - } - } - - - /// - /// Indicates whether the stream can be read. - /// - /// - /// The return value depends on whether the captive stream supports reading. - /// - public override bool CanRead - { - get - { - if (_disposed) throw new ObjectDisposedException("ZlibStream"); - return _baseStream._stream.CanRead; - } - } - - /// - /// Indicates whether the stream supports Seek operations. - /// - /// - /// Always returns false. - /// - public override bool CanSeek - { - get { return false; } - } - - /// - /// Indicates whether the stream can be written. - /// - /// - /// The return value depends on whether the captive stream supports writing. - /// - public override bool CanWrite - { - get - { - if (_disposed) throw new ObjectDisposedException("ZlibStream"); - return _baseStream._stream.CanWrite; - } - } - - /// - /// Flush the stream. - /// - public override void Flush() - { - if (_disposed) throw new ObjectDisposedException("ZlibStream"); - _baseStream.Flush(); - } - - /// - /// Reading this property always throws a NotImplementedException. - /// - public override long Length - { - get { throw new NotImplementedException(); } - } - - /// - /// The position of the stream pointer. - /// - /// - /// Writing this property always throws a NotImplementedException. Reading will - /// return the total bytes written out, if used in writing, or the total bytes - /// read in, if used in reading. The count may refer to compressed bytes or - /// uncompressed bytes, depending on how you've used the stream. - /// - public override long Position - { - get - { - if (this._baseStream._streamMode == ZlibBaseStream.StreamMode.Writer) - return this._baseStream._z.TotalBytesOut; - if (this._baseStream._streamMode == ZlibBaseStream.StreamMode.Reader) - return this._baseStream._z.TotalBytesIn; - return 0; - } - - set { throw new NotImplementedException(); } - } - - /// - /// Read data from the stream. - /// - /// - /// - /// - /// - /// If you wish to use the ZlibStream to compress data while reading, you can create a - /// ZlibStream with CompressionMode.Compress, providing an uncompressed data stream. Then - /// call Read() on that ZlibStream, and the data read will be compressed. If you wish to - /// use the ZlibStream to decompress data while reading, you can create a ZlibStream with - /// CompressionMode.Decompress, providing a readable compressed data stream. Then call - /// Read() on that ZlibStream, and the data will be decompressed as it is read. - /// - /// - /// - /// A ZlibStream can be used for Read() or Write(), but not both. - /// - /// - /// The buffer into which the read data should be placed. - /// the offset within that data array to put the first byte read. - /// the number of bytes to read. - public override int Read(byte[] buffer, int offset, int count) - { - if (_disposed) throw new ObjectDisposedException("ZlibStream"); - return _baseStream.Read(buffer, offset, count); - } - - /// - /// Calling this method always throws a NotImplementedException. - /// - public override long Seek(long offset, System.IO.SeekOrigin origin) - { - throw new NotImplementedException(); - } - - /// - /// Calling this method always throws a NotImplementedException. - /// - public override void SetLength(long value) - { - throw new NotImplementedException(); - } - - /// - /// Write data to the stream. - /// - /// - /// - /// - /// - /// If you wish to use the ZlibStream to compress data while writing, you can create a - /// ZlibStream with CompressionMode.Compress, and a writable output stream. Then call - /// Write() on that ZlibStream, providing uncompressed data as input. The data sent to - /// the output stream will be the compressed form of the data written. If you wish to use - /// the ZlibStream to decompress data while writing, you can create a ZlibStream with - /// CompressionMode.Decompress, and a writable output stream. Then call Write() on that - /// stream, providing previously compressed data. The data sent to the output stream will - /// be the decompressed form of the data written. - /// - /// - /// - /// A ZlibStream can be used for Read() or Write(), but not both. - /// - /// - /// The buffer holding data to write to the stream. - /// the offset within that data array to find the first byte to write. - /// the number of bytes to write. - public override void Write(byte[] buffer, int offset, int count) - { - if (_disposed) throw new ObjectDisposedException("ZlibStream"); - _baseStream.Write(buffer, offset, count); - } - #endregion - - - /// - /// Uncompress a byte array into a single string. - /// - /// - /// - /// A buffer containing ZLIB-compressed data. - /// - public static String UncompressString(byte[] compressed) - { - // workitem 8460 - byte[] working = new byte[1024]; - var encoding = System.Text.Encoding.UTF8; - using (var output = new MemoryStream()) - { - using (var input = new MemoryStream(compressed)) - { - using (Stream decompressor = new ZlibStream(input)) - { - int n; - while ((n = decompressor.Read(working, 0, working.Length)) != 0) - { - output.Write(working, 0, n); - } - } - // reset to allow read from start - output.Seek(0, SeekOrigin.Begin); - var sr = new StreamReader(output, encoding); - return sr.ReadToEnd(); - } - } - } - - /// - /// Uncompress a byte array into a byte array. - /// - /// - /// - /// - /// A buffer containing ZLIB-compressed data. - /// - public static byte[] UncompressBuffer(byte[] compressed) - { - // workitem 8460 - byte[] working = new byte[1024]; - using (var output = new MemoryStream()) - { - using (var input = new MemoryStream(compressed)) - { - using (Stream decompressor = new ZlibStream(input)) - { - int n; - while ((n = decompressor.Read(working, 0, working.Length)) != 0) - { - output.Write(working, 0, n); - } - } - return output.ToArray(); - } - } - } - } - - - internal enum ZlibStreamFlavor { ZLIB = 1950, DEFLATE = 1951, GZIP = 1952 } - - internal class ZlibBaseStream : System.IO.Stream - { - protected internal ZlibCodec _z = null; // deferred init... new ZlibCodec(); - - protected internal StreamMode _streamMode = StreamMode.Undefined; - protected internal FlushType _flushMode; - protected internal ZlibStreamFlavor _flavor; - protected internal bool _leaveOpen; - protected internal byte[] _workingBuffer; - protected internal int _bufferSize = ZlibConstants.WorkingBufferSizeDefault; - protected internal byte[] _buf1 = new byte[1]; - - protected internal System.IO.Stream _stream; - - // workitem 7159 - CRC32 crc; - protected internal string _GzipFileName; - protected internal string _GzipComment; - protected internal DateTime _GzipMtime; - protected internal int _gzipHeaderByteCount; - - internal int Crc32 { get { if (crc == null) return 0; return crc.Crc32Result; } } - - public ZlibBaseStream(System.IO.Stream stream, ZlibStreamFlavor flavor, bool leaveOpen) - : base() - { - this._flushMode = FlushType.None; - //this._workingBuffer = new byte[WORKING_BUFFER_SIZE_DEFAULT]; - this._stream = stream; - this._leaveOpen = leaveOpen; - this._flavor = flavor; - // workitem 7159 - if (flavor == ZlibStreamFlavor.GZIP) - { - crc = new CRC32(); - } - } - - private ZlibCodec z - { - get - { - if (_z == null) - { - bool wantRfc1950Header = (this._flavor == ZlibStreamFlavor.ZLIB); - _z = new ZlibCodec(); - _z.InitializeInflate(wantRfc1950Header); - } - return _z; - } - } - - - - private byte[] workingBuffer - { - get - { - if (_workingBuffer == null) - _workingBuffer = new byte[_bufferSize]; - return _workingBuffer; - } - } - - - // workitem 7813 - totally unnecessary - // public override void WriteByte(byte b) - // { - // _buf1[0] = (byte)b; - // // workitem 7159 - // if (crc != null) - // crc.SlurpBlock(_buf1, 0, 1); - // Write(_buf1, 0, 1); - // } - - - - public override void Write(System.Byte[] buffer, int offset, int count) - { - // workitem 7159 - // calculate the CRC on the unccompressed data (before writing) - if (crc != null) - crc.SlurpBlock(buffer, offset, count); - - if (_streamMode == StreamMode.Undefined) - _streamMode = StreamMode.Writer; - else if (_streamMode != StreamMode.Writer) - throw new ZlibException("Cannot Write after Reading."); - - if (count == 0) - return; - - // first reference of z property will initialize the private var _z - z.InputBuffer = buffer; - _z.NextIn = offset; - _z.AvailableBytesIn = count; - bool done = false; - do - { - _z.OutputBuffer = workingBuffer; - _z.NextOut = 0; - _z.AvailableBytesOut = _workingBuffer.Length; - //int rc = (_wantCompress) - // ? _z.Deflate(_flushMode) - // : _z.Inflate(_flushMode); - int rc = _z.Inflate(_flushMode); - if (rc != ZlibConstants.Z_OK && rc != ZlibConstants.Z_STREAM_END) - throw new ZlibException("inflating: " + _z.Message); - - _stream.Write(_workingBuffer, 0, _workingBuffer.Length - _z.AvailableBytesOut); - - done = _z.AvailableBytesIn == 0 && _z.AvailableBytesOut != 0; - - // If GZIP and de-compress, we're done when 8 bytes remain. - if (_flavor == ZlibStreamFlavor.GZIP) - done = (_z.AvailableBytesIn == 8 && _z.AvailableBytesOut != 0); - - } - while (!done); - } - - - - private void finish() - { - if (_z == null) return; - - if (_streamMode == StreamMode.Writer) - { - bool done = false; - do - { - _z.OutputBuffer = workingBuffer; - _z.NextOut = 0; - _z.AvailableBytesOut = _workingBuffer.Length; - //int rc = (_wantCompress) - // ? _z.Deflate(FlushType.Finish) - // : _z.Inflate(FlushType.Finish); - int rc = _z.Inflate(FlushType.Finish); - if (rc != ZlibConstants.Z_STREAM_END && rc != ZlibConstants.Z_OK) - throw new ZlibException("inflating: " + _z.Message); - - if (_workingBuffer.Length - _z.AvailableBytesOut > 0) - { - _stream.Write(_workingBuffer, 0, _workingBuffer.Length - _z.AvailableBytesOut); - } - - done = _z.AvailableBytesIn == 0 && _z.AvailableBytesOut != 0; - // If GZIP and de-compress, we're done when 8 bytes remain. - if (_flavor == ZlibStreamFlavor.GZIP) - done = (_z.AvailableBytesIn == 8 && _z.AvailableBytesOut != 0); - - } - while (!done); - - Flush(); - - // workitem 7159 - if (_flavor == ZlibStreamFlavor.GZIP) - { - //Console.WriteLine("GZipStream: Last write"); - throw new ZlibException("Writing with decompression is not supported."); - } - } - // workitem 7159 - else if (_streamMode == StreamMode.Reader) - { - if (_flavor == ZlibStreamFlavor.GZIP) - { - // workitem 8501: handle edge case (decompress empty stream) - if (_z.TotalBytesOut == 0L) - return; - - // Read and potentially verify the GZIP trailer: CRC32 and size mod 2^32 - byte[] trailer = new byte[8]; - - if (_z.AvailableBytesIn != 8) - throw new ZlibException(String.Format("Protocol error. AvailableBytesIn={0}, expected 8", - _z.AvailableBytesIn)); - - Array.Copy(_z.InputBuffer, _z.NextIn, trailer, 0, trailer.Length); - - Int32 crc32_expected = BitConverter.ToInt32(trailer, 0); - int crc32_actual = crc.Crc32Result; - Int32 isize_expected = BitConverter.ToInt32(trailer, 4); - Int32 isize_actual = (Int32)(_z.TotalBytesOut & 0x00000000FFFFFFFF); - - // Console.WriteLine("GZipStream: slurped trailer crc(0x{0:X8}) isize({1})", crc32_expected, isize_expected); - // Console.WriteLine("GZipStream: calc'd data crc(0x{0:X8}) isize({1})", crc32_actual, isize_actual); - - if (crc32_actual != crc32_expected) - throw new ZlibException(String.Format("Bad CRC32 in GZIP stream. (actual({0:X8})!=expected({1:X8}))", crc32_actual, crc32_expected)); - - if (isize_actual != isize_expected) - throw new ZlibException(String.Format("Bad size in GZIP stream. (actual({0})!=expected({1}))", isize_actual, isize_expected)); - } - } - } - - - private void end() - { - if (z == null) - return; - //if (_wantCompress) - //{ - // _z.EndDeflate(); - //} - //else - { - _z.EndInflate(); - } - _z = null; - } - - - public override void Close() - { - if (_stream == null) return; - try - { - finish(); - } - finally - { - end(); - if (!_leaveOpen) _stream.Close(); - _stream = null; - } - } - - public override void Flush() - { - _stream.Flush(); - } - - public override System.Int64 Seek(System.Int64 offset, System.IO.SeekOrigin origin) - { - throw new NotImplementedException(); - //_outStream.Seek(offset, origin); - } - public override void SetLength(System.Int64 value) - { - _stream.SetLength(value); - } - - -#if NOT - public int Read() - { - if (Read(_buf1, 0, 1) == 0) - return 0; - // calculate CRC after reading - if (crc!=null) - crc.SlurpBlock(_buf1,0,1); - return (_buf1[0] & 0xFF); - } -#endif - - private bool nomoreinput = false; - - - - private string ReadZeroTerminatedString() - { - var list = new System.Collections.Generic.List(); - bool done = false; - do - { - // workitem 7740 - int n = _stream.Read(_buf1, 0, 1); - if (n != 1) - throw new ZlibException("Unexpected EOF reading GZIP header."); - else - { - if (_buf1[0] == 0) - done = true; - else - list.Add(_buf1[0]); - } - } while (!done); - byte[] a = list.ToArray(); - return GZipStream.iso8859dash1.GetString(a, 0, a.Length); - } - - - private int _ReadAndValidateGzipHeader() - { - int totalBytesRead = 0; - // read the header on the first read - byte[] header = new byte[10]; - int n = _stream.Read(header, 0, header.Length); - - // workitem 8501: handle edge case (decompress empty stream) - if (n == 0) - return 0; - - if (n != 10) - throw new ZlibException("Not a valid GZIP stream."); - - if (header[0] != 0x1F || header[1] != 0x8B || header[2] != 8) - throw new ZlibException("Bad GZIP header."); - - Int32 timet = BitConverter.ToInt32(header, 4); - _GzipMtime = GZipStream._unixEpoch.AddSeconds(timet); - totalBytesRead += n; - if ((header[3] & 0x04) == 0x04) - { - // read and discard extra field - n = _stream.Read(header, 0, 2); // 2-byte length field - totalBytesRead += n; - - Int16 extraLength = (Int16)(header[0] + header[1] * 256); - byte[] extra = new byte[extraLength]; - n = _stream.Read(extra, 0, extra.Length); - if (n != extraLength) - throw new ZlibException("Unexpected end-of-file reading GZIP header."); - totalBytesRead += n; - } - if ((header[3] & 0x08) == 0x08) - _GzipFileName = ReadZeroTerminatedString(); - if ((header[3] & 0x10) == 0x010) - _GzipComment = ReadZeroTerminatedString(); - if ((header[3] & 0x02) == 0x02) - Read(_buf1, 0, 1); // CRC16, ignore - - return totalBytesRead; - } - - - - public override System.Int32 Read(System.Byte[] buffer, System.Int32 offset, System.Int32 count) - { - // According to MS documentation, any implementation of the IO.Stream.Read function must: - // (a) throw an exception if offset & count reference an invalid part of the buffer, - // or if count < 0, or if buffer is null - // (b) return 0 only upon EOF, or if count = 0 - // (c) if not EOF, then return at least 1 byte, up to bytes - - if (_streamMode == StreamMode.Undefined) - { - if (!this._stream.CanRead) throw new ZlibException("The stream is not readable."); - // for the first read, set up some controls. - _streamMode = StreamMode.Reader; - // (The first reference to _z goes through the private accessor which - // may initialize it.) - z.AvailableBytesIn = 0; - if (_flavor == ZlibStreamFlavor.GZIP) - { - _gzipHeaderByteCount = _ReadAndValidateGzipHeader(); - // workitem 8501: handle edge case (decompress empty stream) - if (_gzipHeaderByteCount == 0) - return 0; - } - } - - if (_streamMode != StreamMode.Reader) - throw new ZlibException("Cannot Read after Writing."); - - if (count == 0) return 0; - if (buffer == null) throw new ArgumentNullException("buffer"); - if (count < 0) throw new ArgumentOutOfRangeException("count"); - if (offset < buffer.GetLowerBound(0)) throw new ArgumentOutOfRangeException("offset"); - if ((offset + count) > buffer.GetLength(0)) throw new ArgumentOutOfRangeException("count"); - - int rc = 0; - - // set up the output of the deflate/inflate codec: - _z.OutputBuffer = buffer; - _z.NextOut = offset; - _z.AvailableBytesOut = count; - - // This is necessary in case _workingBuffer has been resized. (new byte[]) - // (The first reference to _workingBuffer goes through the private accessor which - // may initialize it.) - _z.InputBuffer = workingBuffer; - - do - { - // need data in _workingBuffer in order to deflate/inflate. Here, we check if we have any. - if ((_z.AvailableBytesIn == 0) && (!nomoreinput)) - { - // No data available, so try to Read data from the captive stream. - _z.NextIn = 0; - _z.AvailableBytesIn = _stream.Read(_workingBuffer, 0, _workingBuffer.Length); - if (_z.AvailableBytesIn == 0) - nomoreinput = true; - - } - // we have data in InputBuffer; now compress or decompress as appropriate - //rc = (_wantCompress) - // ? _z.Deflate(_flushMode) - // : _z.Inflate(_flushMode); - rc = _z.Inflate(_flushMode); - if (nomoreinput && (rc == ZlibConstants.Z_BUF_ERROR)) - return 0; - - if (rc != ZlibConstants.Z_OK && rc != ZlibConstants.Z_STREAM_END) - throw new ZlibException(String.Format("inflating: rc={0} msg={1}", rc, _z.Message)); - - if ((nomoreinput || rc == ZlibConstants.Z_STREAM_END) && (_z.AvailableBytesOut == count)) - break; // nothing more to read - } - //while (_z.AvailableBytesOut == count && rc == ZlibConstants.Z_OK); - while (_z.AvailableBytesOut > 0 && !nomoreinput && rc == ZlibConstants.Z_OK); - - - // workitem 8557 - // is there more room in output? - if (_z.AvailableBytesOut > 0) - { - if (rc == ZlibConstants.Z_OK && _z.AvailableBytesIn == 0) - { - // deferred - } - - // are we completely done reading? - if (nomoreinput) - { - // and in compression? - /*if (_wantCompress) - { - // no more input data available; therefore we flush to - // try to complete the read - rc = _z.Deflate(FlushType.Finish); - - if (rc != ZlibConstants.Z_OK && rc != ZlibConstants.Z_STREAM_END) - throw new ZlibException(String.Format("Deflating: rc={0} msg={1}", rc, _z.Message)); - }*/ - } - } - - - rc = (count - _z.AvailableBytesOut); - - // calculate CRC after reading - if (crc != null) - crc.SlurpBlock(buffer, offset, rc); - - return rc; - } - - - - public override System.Boolean CanRead - { - get { return this._stream.CanRead; } - } - - public override System.Boolean CanSeek - { - get { return this._stream.CanSeek; } - } - - public override System.Boolean CanWrite - { - get { return this._stream.CanWrite; } - } - - public override System.Int64 Length - { - get { return _stream.Length; } - } - - public override long Position - { - get { throw new NotImplementedException(); } - set { throw new NotImplementedException(); } - } - - internal enum StreamMode - { - Writer, - Reader, - Undefined, - } - } - -} - -#endif \ No newline at end of file diff --git a/RestSharp/Deserializers/DeserializeAsAttribute.cs b/RestSharp/Deserializers/DeserializeAsAttribute.cs deleted file mode 100644 index 390996105..000000000 --- a/RestSharp/Deserializers/DeserializeAsAttribute.cs +++ /dev/null @@ -1,38 +0,0 @@ -#region License -// Copyright 2010 John Sheehan -// -// 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. -#endregion - -using System; - -namespace RestSharp.Deserializers -{ - /// - /// Allows control how class and property names and values are deserialized by XmlAttributeDeserializer - /// - [AttributeUsage(AttributeTargets.Property | AttributeTargets.Class, Inherited = false, AllowMultiple = false)] - public sealed class DeserializeAsAttribute : Attribute - { - /// - /// The name to use for the serialized element - /// - public string Name { get; set; } - - /// - /// Sets if the property to Deserialize is an Attribute or Element (Default: false) - /// - public bool Attribute { get; set; } - } - -} diff --git a/RestSharp/Deserializers/DotNetXmlDeserializer.cs b/RestSharp/Deserializers/DotNetXmlDeserializer.cs deleted file mode 100644 index fbfc46deb..000000000 --- a/RestSharp/Deserializers/DotNetXmlDeserializer.cs +++ /dev/null @@ -1,47 +0,0 @@ -#region License -// Copyright 2010 John Sheehan -// -// 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. -#endregion - -using System.IO; -using System.Text; - -namespace RestSharp.Deserializers -{ - /// - /// Wrapper for System.Xml.Serialization.XmlSerializer. - /// - public class DotNetXmlDeserializer : IDeserializer - { - public string DateFormat { get; set; } - - public string Namespace { get; set; } - - public string RootElement { get; set; } - - public T Deserialize(IRestResponse response) - { - if (string.IsNullOrEmpty(response.Content)) - { - return default(T); - } - - using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(response.Content))) - { - var serializer = new System.Xml.Serialization.XmlSerializer(typeof(T)); - return (T)serializer.Deserialize(stream); - } - } - } -} \ No newline at end of file diff --git a/RestSharp/Deserializers/JsonDeserializer.cs b/RestSharp/Deserializers/JsonDeserializer.cs deleted file mode 100644 index e1df80610..000000000 --- a/RestSharp/Deserializers/JsonDeserializer.cs +++ /dev/null @@ -1,288 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using RestSharp.Extensions; - -namespace RestSharp.Deserializers -{ - public class JsonDeserializer : IDeserializer - { - public string RootElement { get; set; } - public string Namespace { get; set; } - public string DateFormat { get; set; } - public CultureInfo Culture { get; set; } - - public JsonDeserializer() - { - Culture = CultureInfo.InvariantCulture; - } - - public T Deserialize(IRestResponse response) - { - var target = Activator.CreateInstance(); - - if (target is IList) - { - var objType = target.GetType(); - - if (RootElement.HasValue()) - { - var root = FindRoot(response.Content); - target = (T)BuildList(objType, root); - } - else - { - var data = SimpleJson.DeserializeObject(response.Content); - target = (T)BuildList(objType, data); - } - } - else if (target is IDictionary) - { - var root = FindRoot(response.Content); - target = (T)BuildDictionary(target.GetType(), root); - } - else - { - var root = FindRoot(response.Content); - Map(target, (IDictionary)root); - } - - return target; - } - - private object FindRoot(string content) - { - var data = (IDictionary)SimpleJson.DeserializeObject(content); - if (RootElement.HasValue() && data.ContainsKey(RootElement)) - { - return data[RootElement]; - } - return data; - } - - private void Map(object target, IDictionary data) - { - var objType = target.GetType(); - var props = objType.GetProperties().Where(p => p.CanWrite).ToList(); - - foreach (var prop in props) - { - var type = prop.PropertyType; - - string name = String.Empty; - - var attributes = prop.GetCustomAttributes(typeof(DeserializeAsAttribute), false); - if (attributes.Length > 0) - { - var attribute = (DeserializeAsAttribute)attributes[0]; - name = attribute.Name; - } - else - { - name = prop.Name; - } - - var parts = name.Split('.'); - var currentData = data; - object value = null; - for (var i = 0; i < parts.Length; ++i) - { - var actualName = parts[i].GetNameVariants(Culture).FirstOrDefault(currentData.ContainsKey); - if (actualName == null) break; - if(i == parts.Length - 1) value = currentData[actualName]; - else currentData = (IDictionary)currentData[actualName]; - } - - if(value != null) prop.SetValue(target, ConvertValue(type, value), null); - } - } - - private IDictionary BuildDictionary(Type type, object parent) - { - var dict = (IDictionary)Activator.CreateInstance(type); - var valueType = type.GetGenericArguments()[1]; - foreach (var child in (IDictionary)parent) - { - var key = child.Key; - object item = null; - if (valueType.IsGenericType && valueType.GetGenericTypeDefinition() == typeof(List<>)) - { - item = BuildList(valueType, child.Value); - } - else - { - item = ConvertValue(valueType, child.Value); - } - dict.Add(key, item); - } - return dict; - } - - private IList BuildList(Type type, object parent) - { - var list = (IList)Activator.CreateInstance(type); - var listType = type.GetInterfaces().First(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IList<>)); - var itemType = listType.GetGenericArguments()[0]; - - if (parent is IList) - { - foreach (var element in (IList)parent) - { - if (itemType.IsPrimitive) - { - var value = element.ToString(); - list.Add(value.ChangeType(itemType, Culture)); - } - else if (itemType == typeof(string)) - { - if (element == null) - { - list.Add(null); - continue; - } - - list.Add(element.ToString()); - } - else - { - if (element == null) - { - list.Add(null); - continue; - } - - var item = ConvertValue(itemType, element); - list.Add(item); - } - } - } - else - { - list.Add(ConvertValue(itemType, parent)); - } - return list; - } - - private object ConvertValue(Type type, object value) - { - var stringValue = Convert.ToString(value, Culture); - - // check for nullable and extract underlying type - if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) - { - // Since the type is nullable and no value is provided return null - if (String.IsNullOrEmpty(stringValue)) return null; - - type = type.GetGenericArguments()[0]; - } - - if (type == typeof(System.Object) && value != null) - { - type = value.GetType(); - } - - if (type.IsPrimitive) - { - return value.ChangeType(type, Culture); - } - else if (type.IsEnum) - { - return type.FindEnumValue(stringValue, Culture); - } - else if (type == typeof(Uri)) - { - return new Uri(stringValue, UriKind.RelativeOrAbsolute); - } - else if (type == typeof(string)) - { - return stringValue; - } - else if (type == typeof(DateTime) -#if !PocketPC - || type == typeof(DateTimeOffset) -#endif - ) - { - DateTime dt; - if (DateFormat.HasValue()) - { - dt = DateTime.ParseExact(stringValue, DateFormat, Culture); - } - else - { - // try parsing instead - dt = stringValue.ParseJsonDate(Culture); - } - -#if PocketPC - return dt; -#else - if (type == typeof(DateTime)) - { - return dt; - } - else if (type == typeof(DateTimeOffset)) - { - return (DateTimeOffset)dt; - } -#endif - } - else if (type == typeof(Decimal)) - { - if (value is double) - return (decimal)((double)value); - - return Decimal.Parse(stringValue, Culture); - } - else if (type == typeof(Guid)) - { - return string.IsNullOrEmpty(stringValue) ? Guid.Empty : new Guid(stringValue); - } - else if (type == typeof(TimeSpan)) - { - return TimeSpan.Parse(stringValue); - } - else if (type.IsGenericType) - { - var genericTypeDef = type.GetGenericTypeDefinition(); - if (genericTypeDef == typeof(List<>)) - { - return BuildList(type, value); - } - else if (genericTypeDef == typeof(Dictionary<,>)) - { - var keyType = type.GetGenericArguments()[0]; - - // only supports Dict() - if (keyType == typeof(string)) - { - return BuildDictionary(type, value); - } - } - else - { - // nested property classes - return CreateAndMap(type, value); - } - } - else - { - // nested property classes - return CreateAndMap(type, value); - } - - return null; - } - - private object CreateAndMap(Type type, object element) - { - var instance = Activator.CreateInstance(type); - - Map(instance, (IDictionary)element); - - return instance; - } - } -} diff --git a/RestSharp/Deserializers/XmlAttributeDeserializer.cs b/RestSharp/Deserializers/XmlAttributeDeserializer.cs deleted file mode 100644 index aea2af8ea..000000000 --- a/RestSharp/Deserializers/XmlAttributeDeserializer.cs +++ /dev/null @@ -1,49 +0,0 @@ -#region License -// Copyright 2010 John Sheehan -// -// 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. -#endregion - -using System.Reflection; -using System.Xml.Linq; -using RestSharp.Extensions; - -namespace RestSharp.Deserializers -{ - public class XmlAttributeDeserializer : XmlDeserializer - { - protected override object GetValueFromXml(XElement root, XName name, PropertyInfo prop) - { - var isAttribute = false; - - //Check for the DeserializeAs attribute on the property - var options = prop.GetAttribute(); - if (options != null) - { - name = options.Name ?? name; - isAttribute = options.Attribute; - } - - if (isAttribute) - { - var attributeVal = GetAttributeByName(root, name); - if (attributeVal != null) - { - return attributeVal.Value; - } - } - - return base.GetValueFromXml(root, name, prop); - } - } -} diff --git a/RestSharp/Deserializers/XmlDeserializer.cs b/RestSharp/Deserializers/XmlDeserializer.cs deleted file mode 100644 index 277b4fee6..000000000 --- a/RestSharp/Deserializers/XmlDeserializer.cs +++ /dev/null @@ -1,466 +0,0 @@ -#region License -// Copyright 2010 John Sheehan -// -// 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. -#endregion - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Xml.Linq; - -using RestSharp.Extensions; -using System.Globalization; -using System.Xml; -using System.ComponentModel; - -namespace RestSharp.Deserializers -{ - public class XmlDeserializer : IDeserializer - { - public string RootElement { get; set; } - public string Namespace { get; set; } - public string DateFormat { get; set; } - public CultureInfo Culture { get; set; } - - public XmlDeserializer() - { - Culture = CultureInfo.InvariantCulture; - } - - public virtual T Deserialize(IRestResponse response) - { - if (string.IsNullOrEmpty( response.Content )) - return default(T); - - var doc = XDocument.Parse(response.Content); - var root = doc.Root; - if (RootElement.HasValue() && doc.Root != null) - { - root = doc.Root.Element(RootElement.AsNamespaced(Namespace)); - } - - // autodetect xml namespace - if (!Namespace.HasValue()) - { - RemoveNamespace(doc); - } - - var x = Activator.CreateInstance(); - var objType = x.GetType(); - - if (objType.IsSubclassOfRawGeneric(typeof(List<>))) - { - x = (T)HandleListDerivative(x, root, objType.Name, objType); - } - else - { - Map(x, root); - } - - return x; - } - - private void RemoveNamespace(XDocument xdoc) - { - foreach (XElement e in xdoc.Root.DescendantsAndSelf()) - { - if (e.Name.Namespace != XNamespace.None) - { - e.Name = XNamespace.None.GetName(e.Name.LocalName); - } - if (e.Attributes().Any(a => a.IsNamespaceDeclaration || a.Name.Namespace != XNamespace.None)) - { - e.ReplaceAttributes(e.Attributes().Select(a => a.IsNamespaceDeclaration ? null : a.Name.Namespace != XNamespace.None ? new XAttribute(XNamespace.None.GetName(a.Name.LocalName), a.Value) : a)); - } - } - } - - protected virtual void Map(object x, XElement root) - { - var objType = x.GetType(); - var props = objType.GetProperties(); - - foreach (var prop in props) - { - var type = prop.PropertyType; - - if (!type.IsPublic || !prop.CanWrite) - continue; - - var name = prop.Name.AsNamespaced(Namespace); - var value = GetValueFromXml(root, name, prop); - - if (value == null) - { - // special case for inline list items - if (type.IsGenericType) - { - var genericType = type.GetGenericArguments()[0]; - var first = GetElementByName(root, genericType.Name); - var list = (IList)Activator.CreateInstance(type); - - if (first != null) - { - var elements = root.Elements(first.Name); - PopulateListFromElements(genericType, elements, list); - } - - prop.SetValue(x, list, null); - } - continue; - } - - // check for nullable and extract underlying type - if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) - { - // if the value is empty, set the property to null... - if (value == null || String.IsNullOrEmpty(value.ToString())) - { - prop.SetValue(x, null, null); - continue; - } - type = type.GetGenericArguments()[0]; - } - - if (type == typeof(bool)) - { - var toConvert = value.ToString().ToLower(); - prop.SetValue(x, XmlConvert.ToBoolean(toConvert), null); - } - else if (type.IsPrimitive) - { - prop.SetValue(x, value.ChangeType(type, Culture), null); - } - else if (type.IsEnum) - { - var converted = type.FindEnumValue(value.ToString(), Culture); - prop.SetValue(x, converted, null); - } - else if (type == typeof(Uri)) - { - var uri = new Uri(value.ToString(), UriKind.RelativeOrAbsolute); - prop.SetValue(x, uri, null); - } - else if (type == typeof(string)) - { - prop.SetValue(x, value, null); - } - else if (type == typeof(DateTime)) - { - if (DateFormat.HasValue()) - { - value = DateTime.ParseExact(value.ToString(), DateFormat, Culture); - } - else - { - value = DateTime.Parse(value.ToString(), Culture); - } - - prop.SetValue(x, value, null); - } -#if !PocketPC - else if (type == typeof(DateTimeOffset)) - { - var toConvert = value.ToString(); - if (!string.IsNullOrEmpty(toConvert)) - { - DateTimeOffset deserialisedValue; - try - { - deserialisedValue = XmlConvert.ToDateTimeOffset(toConvert); - prop.SetValue(x, deserialisedValue, null); - } - catch (Exception) - { - object result; - if (TryGetFromString(toConvert, out result, type)) - { - prop.SetValue(x, result, null); - } - else - { - //fallback to parse - deserialisedValue = DateTimeOffset.Parse(toConvert); - prop.SetValue(x, deserialisedValue, null); - } - } - } - } -#endif - else if (type == typeof(Decimal)) - { - value = Decimal.Parse(value.ToString(), Culture); - prop.SetValue(x, value, null); - } - else if (type == typeof(Guid)) - { - var raw = value.ToString(); - value = string.IsNullOrEmpty(raw) ? Guid.Empty : new Guid(value.ToString()); - prop.SetValue(x, value, null); - } - else if (type == typeof(TimeSpan)) - { - var timeSpan = XmlConvert.ToTimeSpan(value.ToString()); - prop.SetValue(x, timeSpan, null); - } - else if (type.IsGenericType) - { - var t = type.GetGenericArguments()[0]; - var list = (IList)Activator.CreateInstance(type); - - var container = GetElementByName(root, prop.Name.AsNamespaced(Namespace)); - - if (container.HasElements) - { - var first = container.Elements().FirstOrDefault(); - var elements = container.Elements(first.Name); - PopulateListFromElements(t, elements, list); - } - - prop.SetValue(x, list, null); - } - else if (type.IsSubclassOfRawGeneric(typeof(List<>))) - { - // handles classes that derive from List - // e.g. a collection that also has attributes - var list = HandleListDerivative(x, root, prop.Name, type); - prop.SetValue(x, list, null); - } - else - { - //fallback to type converters if possible - object result; - if (TryGetFromString(value.ToString(), out result, type)) - { - prop.SetValue(x, result, null); - } - else - { - // nested property classes - if (root != null) - { - var element = GetElementByName(root, name); - if (element != null) - { - var item = CreateAndMap(type, element); - prop.SetValue(x, item, null); - } - } - } - } - } - } - - private static bool TryGetFromString(string inputString, out object result, Type type) - { -#if !SILVERLIGHT && !WINDOWS_PHONE && !PocketPC - var converter = TypeDescriptor.GetConverter(type); - if (converter.CanConvertFrom(typeof(string))) - { - result = (converter.ConvertFromInvariantString(inputString)); - return true; - } - result = null; - return false; -#else - result = null; - return false; -#endif - } - - private void PopulateListFromElements(Type t, IEnumerable elements, IList list) - { - foreach (var element in elements) - { - var item = CreateAndMap(t, element); - list.Add(item); - } - } - - private object HandleListDerivative(object x, XElement root, string propName, Type type) - { - Type t; - - if (type.IsGenericType) - { - t = type.GetGenericArguments()[0]; - } - else - { - t = type.BaseType.GetGenericArguments()[0]; - } - - - var list = (IList)Activator.CreateInstance(type); - - var elements = root.Descendants(t.Name.AsNamespaced(Namespace)); - - var name = t.Name; - - if (!elements.Any()) - { - var lowerName = name.ToLower().AsNamespaced(Namespace); - elements = root.Descendants(lowerName); - } - - if (!elements.Any()) - { - var camelName = name.ToCamelCase(Culture).AsNamespaced(Namespace); - elements = root.Descendants(camelName); - } - - if (!elements.Any()) - { - elements = root.Descendants().Where(e => e.Name.LocalName.RemoveUnderscoresAndDashes() == name); - } - - if (!elements.Any()) - { - var lowerName = name.ToLower().AsNamespaced(Namespace); - elements = root.Descendants().Where(e => e.Name.LocalName.RemoveUnderscoresAndDashes() == lowerName); - } - - PopulateListFromElements(t, elements, list); - - // get properties too, not just list items - // only if this isn't a generic type - if (!type.IsGenericType) - { - Map(list, root.Element(propName.AsNamespaced(Namespace)) ?? root); // when using RootElement, the heirarchy is different - } - - return list; - } - - protected virtual object CreateAndMap(Type t, XElement element) - { - object item; - if (t == typeof(String)) - { - item = element.Value; - } - else if (t.IsPrimitive) - { - item = element.Value.ChangeType(t, Culture); - } - else - { - item = Activator.CreateInstance(t); - Map(item, element); - } - - return item; - } - - protected virtual object GetValueFromXml(XElement root, XName name, PropertyInfo prop) - { - object val = null; - - if (root != null) - { - var element = GetElementByName(root, name); - if (element == null) - { - var attribute = GetAttributeByName(root, name); - if (attribute != null) - { - val = attribute.Value; - } - } - else - { - if (!element.IsEmpty || element.HasElements || element.HasAttributes) - { - val = element.Value; - } - } - } - - return val; - } - - protected virtual XElement GetElementByName(XElement root, XName name) - { - var lowerName = name.LocalName.ToLower().AsNamespaced(name.NamespaceName); - var camelName = name.LocalName.ToCamelCase(Culture).AsNamespaced(name.NamespaceName); - - if (root.Element(name) != null) - { - return root.Element(name); - } - - if (root.Element(lowerName) != null) - { - return root.Element(lowerName); - } - - if (root.Element(camelName) != null) - { - return root.Element(camelName); - } - - if (name == "Value".AsNamespaced(name.NamespaceName)) - { - return root; - } - - // try looking for element that matches sanitized property name (Order by depth) - var element = root.Descendants() - .OrderBy(d => d.Ancestors().Count()) - .FirstOrDefault(d => d.Name.LocalName.RemoveUnderscoresAndDashes() == name.LocalName) - ?? root.Descendants() - .OrderBy(d => d.Ancestors().Count()) - .FirstOrDefault(d => d.Name.LocalName.RemoveUnderscoresAndDashes() == name.LocalName.ToLower()); - - if (element != null) - { - return element; - } - - return null; - } - - protected virtual XAttribute GetAttributeByName(XElement root, XName name) - { - var lowerName = name.LocalName.ToLower().AsNamespaced(name.NamespaceName); - var camelName = name.LocalName.ToCamelCase(Culture).AsNamespaced(name.NamespaceName); - - if (root.Attribute(name) != null) - { - return root.Attribute(name); - } - - if (root.Attribute(lowerName) != null) - { - return root.Attribute(lowerName); - } - - if (root.Attribute(camelName) != null) - { - return root.Attribute(camelName); - } - - // try looking for element that matches sanitized property name - var element = root.Attributes().FirstOrDefault(d => d.Name.LocalName.RemoveUnderscoresAndDashes() == name.LocalName); - if (element != null) - { - return element; - } - - return null; - } - } -} diff --git a/RestSharp/Enum.cs b/RestSharp/Enum.cs deleted file mode 100644 index ad62429e3..000000000 --- a/RestSharp/Enum.cs +++ /dev/null @@ -1,81 +0,0 @@ -#region License -// Copyright 2010 John Sheehan -// -// 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. -#endregion - -namespace RestSharp -{ - /// - /// Types of parameters that can be added to requests - /// - public enum ParameterType - { - Cookie, - GetOrPost, - UrlSegment, - HttpHeader, - RequestBody, - QueryString - } - - /// - /// Data formats - /// - public enum DataFormat - { - Json, - Xml - } - - /// - /// HTTP method to use when making requests - /// - public enum Method - { - GET, - POST, - PUT, - DELETE, - HEAD, - OPTIONS, - PATCH - } - - /// - /// Format strings for commonly-used date formats - /// - public struct DateFormat - { - /// - /// .NET format string for ISO 8601 date format - /// - public const string Iso8601 = "s"; - /// - /// .NET format string for roundtrip date format - /// - public const string RoundTrip = "u"; - } - - /// - /// Status for responses (surprised?) - /// - public enum ResponseStatus - { - None, - Completed, - Error, - TimedOut, - Aborted - } -} diff --git a/RestSharp/Extensions/MiscExtensions.cs b/RestSharp/Extensions/MiscExtensions.cs deleted file mode 100644 index 110fdba7a..000000000 --- a/RestSharp/Extensions/MiscExtensions.cs +++ /dev/null @@ -1,128 +0,0 @@ -#region License -// Copyright 2010 John Sheehan -// -// 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. -#endregion - -using System.Globalization; -using System.IO; -using System.Text; - -namespace RestSharp.Extensions -{ - /// - /// Extension method overload! - /// - public static class MiscExtensions - { -#if !WINDOWS_PHONE && !PocketPC - /// - /// Save a byte array to a file - /// - /// Bytes to save - /// Full path to save file to - public static void SaveAs(this byte[] input, string path) - { - File.WriteAllBytes(path, input); - } -#endif - - /// - /// Read a stream into a byte array - /// - /// Stream to read - /// byte[] - public static byte[] ReadAsBytes(this Stream input) - { - byte[] buffer = new byte[16 * 1024]; - using (MemoryStream ms = new MemoryStream()) - { - int read; - while ((read = input.Read(buffer, 0, buffer.Length)) > 0) - { - ms.Write(buffer, 0, read); - } - return ms.ToArray(); - } - } - - /// - /// Copies bytes from one stream to another - /// - /// The input stream. - /// The output stream. - public static void CopyTo(this Stream input, Stream output) - { - var buffer = new byte[32768]; - while(true) - { - var read = input.Read(buffer, 0, buffer.Length); - if(read <= 0) - return; - output.Write(buffer, 0, read); - } - } - - /// - /// Converts a byte array to a string, using its byte order mark to convert it to the right encoding. - /// http://www.shrinkrays.net/code-snippets/csharp/an-extension-method-for-converting-a-byte-array-to-a-string.aspx - /// - /// An array of bytes to convert - /// The byte as a string. - public static string AsString(this byte[] buffer) - { - if (buffer == null) return ""; - - // Ansi as default - Encoding encoding = Encoding.UTF8; - -#if FRAMEWORK - return encoding.GetString(buffer, 0, buffer.Length); -#else - if (buffer == null || buffer.Length == 0) - return ""; - - /* - EF BB BF UTF-8 - FF FE UTF-16 little endian - FE FF UTF-16 big endian - FF FE 00 00 UTF-32, little endian - 00 00 FE FF UTF-32, big-endian - */ - - if (buffer[0] == 0xef && buffer[1] == 0xbb && buffer[2] == 0xbf) - { - encoding = Encoding.UTF8; - } - else if (buffer[0] == 0xfe && buffer[1] == 0xff) - { - encoding = Encoding.Unicode; - } - else if (buffer[0] == 0xfe && buffer[1] == 0xff) - { - encoding = Encoding.BigEndianUnicode; // utf-16be - } - - using (MemoryStream stream = new MemoryStream()) - { - stream.Write(buffer, 0, buffer.Length); - stream.Seek(0, SeekOrigin.Begin); - using (StreamReader reader = new StreamReader(stream, encoding)) - { - return reader.ReadToEnd(); - } - } -#endif - } - } -} \ No newline at end of file diff --git a/RestSharp/Extensions/MonoHttp/Helpers.cs b/RestSharp/Extensions/MonoHttp/Helpers.cs deleted file mode 100644 index 9ba9baf30..000000000 --- a/RestSharp/Extensions/MonoHttp/Helpers.cs +++ /dev/null @@ -1,38 +0,0 @@ -// -// System.Web.Util.Helpers -// -// Authors: -// Marek Habersack (mhabersack@novell.com) -// -// (C) 2009 Novell, Inc (http://novell.com) - -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// -using System; -using System.Globalization; - -namespace RestSharp.Contrib -{ - class Helpers - { - public static readonly CultureInfo InvariantCulture = CultureInfo.InvariantCulture; - } -} \ No newline at end of file diff --git a/RestSharp/Extensions/MonoHttp/HtmlEncoder.cs b/RestSharp/Extensions/MonoHttp/HtmlEncoder.cs deleted file mode 100644 index e8113ca93..000000000 --- a/RestSharp/Extensions/MonoHttp/HtmlEncoder.cs +++ /dev/null @@ -1,926 +0,0 @@ -// -// Authors: -// Patrik Torstensson (Patrik.Torstensson@labs2.com) -// Wictor Wilén (decode/encode functions) (wictor@ibizkit.se) -// Tim Coleman (tim@timcoleman.com) -// Gonzalo Paniagua Javier (gonzalo@ximian.com) - -// Marek Habersack -// -// (C) 2005-2010 Novell, Inc (http://novell.com/) -// - -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// -using System; -using System.Collections.Generic; -using System.Configuration; -using System.IO; -using System.Text; -#if NET_4_0 -using System.Web.Configuration; -#endif - -namespace RestSharp.Contrib -{ -#if NET_4_0 - public -#endif - class HttpEncoder - { - static char[] hexChars = "0123456789abcdef".ToCharArray(); - static object entitiesLock = new object(); -#if PocketPC - static Dictionary entities; -#else - static SortedDictionary entities; -#endif -#if NET_4_0 - static Lazy defaultEncoder; - static Lazy currentEncoderLazy; -#else - static HttpEncoder defaultEncoder; -#endif - static HttpEncoder currentEncoder; - - static IDictionary Entities - { - get - { - lock (entitiesLock) - { - if (entities == null) - InitEntities(); - - return entities; - } - } - } - - public static HttpEncoder Current - { - get - { -#if NET_4_0 - if (currentEncoder == null) - currentEncoder = currentEncoderLazy.Value; -#endif - return currentEncoder; - } -#if NET_4_0 - set { - if (value == null) - throw new ArgumentNullException ("value"); - currentEncoder = value; - } -#endif - } - - public static HttpEncoder Default - { - get - { -#if NET_4_0 - return defaultEncoder.Value; -#else - return defaultEncoder; -#endif - } - } - - static HttpEncoder() - { -#if NET_4_0 - defaultEncoder = new Lazy (() => new HttpEncoder ()); - currentEncoderLazy = new Lazy (new Func (GetCustomEncoderFromConfig)); -#else - defaultEncoder = new HttpEncoder(); - currentEncoder = defaultEncoder; -#endif - } - - public HttpEncoder() - { - } -#if NET_4_0 - protected internal virtual -#else - internal static -#endif - void HeaderNameValueEncode(string headerName, string headerValue, out string encodedHeaderName, out string encodedHeaderValue) - { - if (String.IsNullOrEmpty(headerName)) - encodedHeaderName = headerName; - else - encodedHeaderName = EncodeHeaderString(headerName); - - if (String.IsNullOrEmpty(headerValue)) - encodedHeaderValue = headerValue; - else - encodedHeaderValue = EncodeHeaderString(headerValue); - } - - static void StringBuilderAppend(string s, ref StringBuilder sb) - { - if (sb == null) - sb = new StringBuilder(s); - else - sb.Append(s); - } - - static string EncodeHeaderString(string input) - { - StringBuilder sb = null; - char ch; - - for (int i = 0; i < input.Length; i++) - { - ch = input[i]; - - if ((ch < 32 && ch != 9) || ch == 127) - StringBuilderAppend(String.Format("%{0:x2}", (int)ch), ref sb); - } - - if (sb != null) - return sb.ToString(); - - return input; - } -#if NET_4_0 - protected internal virtual void HtmlAttributeEncode (string value, TextWriter output) - { - - if (output == null) - throw new ArgumentNullException ("output"); - - if (String.IsNullOrEmpty (value)) - return; - - output.Write (HtmlAttributeEncode (value)); - } - - protected internal virtual void HtmlDecode (string value, TextWriter output) - { - if (output == null) - throw new ArgumentNullException ("output"); - - output.Write (HtmlDecode (value)); - } - - protected internal virtual void HtmlEncode (string value, TextWriter output) - { - if (output == null) - throw new ArgumentNullException ("output"); - - output.Write (HtmlEncode (value)); - } - - protected internal virtual byte[] UrlEncode (byte[] bytes, int offset, int count) - { - return UrlEncodeToBytes (bytes, offset, count); - } - - static HttpEncoder GetCustomEncoderFromConfig () - { - var cfg = WebConfigurationManager.GetSection ("system.web/httpRuntime") as HttpRuntimeSection; - string typeName = cfg.EncoderType; - - if (String.Compare (typeName, "System.Web.Util.HttpEncoder", StringComparison.OrdinalIgnoreCase) == 0) - return Default; - - Type t = Type.GetType (typeName, false); - if (t == null) - throw new ConfigurationErrorsException (String.Format ("Could not load type '{0}'.", typeName)); - - if (!typeof (HttpEncoder).IsAssignableFrom (t)) - throw new ConfigurationErrorsException ( - String.Format ("'{0}' is not allowed here because it does not extend class 'System.Web.Util.HttpEncoder'.", typeName) - ); - - return Activator.CreateInstance (t, false) as HttpEncoder; - } -#endif -#if NET_4_0 - protected internal virtual -#else - internal static -#endif - string UrlPathEncode(string value) - { - if (String.IsNullOrEmpty(value)) - return value; - - MemoryStream result = new MemoryStream(); - int length = value.Length; - for (int i = 0; i < length; i++) - UrlPathEncodeChar(value[i], result); - - byte[] bytes = result.ToArray(); - return Encoding.ASCII.GetString(bytes, 0, bytes.Length); - } - - internal static byte[] UrlEncodeToBytes(byte[] bytes, int offset, int count) - { - if (bytes == null) - throw new ArgumentNullException("bytes"); - - int blen = bytes.Length; - if (blen == 0) - return new byte[0]; - - if (offset < 0 || offset >= blen) - throw new ArgumentOutOfRangeException("offset"); - - if (count < 0 || count > blen - offset) - throw new ArgumentOutOfRangeException("count"); - - MemoryStream result = new MemoryStream(count); - int end = offset + count; - for (int i = offset; i < end; i++) - UrlEncodeChar((char)bytes[i], result, false); - - return result.ToArray(); - } - - internal static string HtmlEncode(string s) - { - if (s == null) - return null; - - if (s.Length == 0) - return String.Empty; - - bool needEncode = false; - for (int i = 0; i < s.Length; i++) - { - char c = s[i]; - if (c == '&' || c == '"' || c == '<' || c == '>' || c > 159 -#if NET_4_0 - || c == '\'' -#endif -) - { - needEncode = true; - break; - } - } - - if (!needEncode) - return s; - - StringBuilder output = new StringBuilder(); - char ch; - int len = s.Length; - - for (int i = 0; i < len; i++) - { - switch (s[i]) - { - case '&': - output.Append("&"); - break; - case '>': - output.Append(">"); - break; - case '<': - output.Append("<"); - break; - case '"': - output.Append("""); - break; -#if NET_4_0 - case '\'': - output.Append ("'"); - break; -#endif - case '\uff1c': - output.Append("<"); - break; - - case '\uff1e': - output.Append(">"); - break; - - default: - ch = s[i]; - if (ch > 159 && ch < 256) - { - output.Append("&#"); - output.Append(((int)ch).ToString(Helpers.InvariantCulture)); - output.Append(";"); - } - else - output.Append(ch); - break; - } - } - - return output.ToString(); - } - - internal static string HtmlAttributeEncode(string s) - { -#if NET_4_0 - if (String.IsNullOrEmpty (s)) - return String.Empty; -#else - if (s == null) - return null; - - if (s.Length == 0) - return String.Empty; -#endif - bool needEncode = false; - for (int i = 0; i < s.Length; i++) - { - char c = s[i]; - if (c == '&' || c == '"' || c == '<' -#if NET_4_0 - || c == '\'' -#endif -) - { - needEncode = true; - break; - } - } - - if (!needEncode) - return s; - - StringBuilder output = new StringBuilder(); - int len = s.Length; - for (int i = 0; i < len; i++) - switch (s[i]) - { - case '&': - output.Append("&"); - break; - case '"': - output.Append("""); - break; - case '<': - output.Append("<"); - break; -#if NET_4_0 - case '\'': - output.Append ("'"); - break; -#endif - default: - output.Append(s[i]); - break; - } - - return output.ToString(); - } - - internal static string HtmlDecode(string s) - { - if (s == null) - return null; - - if (s.Length == 0) - return String.Empty; - - if (s.IndexOf('&') == -1) - return s; -#if NET_4_0 - StringBuilder rawEntity = new StringBuilder (); -#endif - StringBuilder entity = new StringBuilder(); - StringBuilder output = new StringBuilder(); - int len = s.Length; - // 0 -> nothing, - // 1 -> right after '&' - // 2 -> between '&' and ';' but no '#' - // 3 -> '#' found after '&' and getting numbers - int state = 0; - int number = 0; - bool is_hex_value = false; - bool have_trailing_digits = false; - - for (int i = 0; i < len; i++) - { - char c = s[i]; - if (state == 0) - { - if (c == '&') - { - entity.Append(c); -#if NET_4_0 - rawEntity.Append (c); -#endif - state = 1; - } - else - { - output.Append(c); - } - continue; - } - - if (c == '&') - { - state = 1; - if (have_trailing_digits) - { - entity.Append(number.ToString(Helpers.InvariantCulture)); - have_trailing_digits = false; - } - - output.Append(entity.ToString()); - entity.Length = 0; - entity.Append('&'); - continue; - } - - if (state == 1) - { - if (c == ';') - { - state = 0; - output.Append(entity.ToString()); - output.Append(c); - entity.Length = 0; - } - else - { - number = 0; - is_hex_value = false; - if (c != '#') - { - state = 2; - } - else - { - state = 3; - } - entity.Append(c); -#if NET_4_0 - rawEntity.Append (c); -#endif - } - } - else if (state == 2) - { - entity.Append(c); - if (c == ';') - { - string key = entity.ToString(); - if (key.Length > 1 && Entities.ContainsKey(key.Substring(1, key.Length - 2))) - key = Entities[key.Substring(1, key.Length - 2)].ToString(); - - output.Append(key); - state = 0; - entity.Length = 0; -#if NET_4_0 - rawEntity.Length = 0; -#endif - } - } - else if (state == 3) - { - if (c == ';') - { -#if NET_4_0 - if (number == 0) - output.Append (rawEntity.ToString () + ";"); - else -#endif - if (number > 65535) - { - output.Append("&#"); - output.Append(number.ToString(Helpers.InvariantCulture)); - output.Append(";"); - } - else - { - output.Append((char)number); - } - state = 0; - entity.Length = 0; -#if NET_4_0 - rawEntity.Length = 0; -#endif - have_trailing_digits = false; - } - else if (is_hex_value && Uri.IsHexDigit(c)) - { - number = number * 16 + Uri.FromHex(c); - have_trailing_digits = true; -#if NET_4_0 - rawEntity.Append (c); -#endif - } - else if (Char.IsDigit(c)) - { - number = number * 10 + ((int)c - '0'); - have_trailing_digits = true; -#if NET_4_0 - rawEntity.Append (c); -#endif - } - else if (number == 0 && (c == 'x' || c == 'X')) - { - is_hex_value = true; -#if NET_4_0 - rawEntity.Append (c); -#endif - } - else - { - state = 2; - if (have_trailing_digits) - { - entity.Append(number.ToString(Helpers.InvariantCulture)); - have_trailing_digits = false; - } - entity.Append(c); - } - } - } - - if (entity.Length > 0) - { - output.Append(entity.ToString()); - } - else if (have_trailing_digits) - { - output.Append(number.ToString(Helpers.InvariantCulture)); - } - return output.ToString(); - } - - internal static bool NotEncoded(char c) - { - return (c == '!' || c == '(' || c == ')' || c == '*' || c == '-' || c == '.' || c == '_' -#if !NET_4_0 - || c == '\'' -#endif -); - } - - internal static void UrlEncodeChar(char c, Stream result, bool isUnicode) - { - if (c > 255) - { - //FIXME: what happens when there is an internal error? - //if (!isUnicode) - // throw new ArgumentOutOfRangeException ("c", c, "c must be less than 256"); - int idx; - int i = (int)c; - - result.WriteByte((byte)'%'); - result.WriteByte((byte)'u'); - idx = i >> 12; - result.WriteByte((byte)hexChars[idx]); - idx = (i >> 8) & 0x0F; - result.WriteByte((byte)hexChars[idx]); - idx = (i >> 4) & 0x0F; - result.WriteByte((byte)hexChars[idx]); - idx = i & 0x0F; - result.WriteByte((byte)hexChars[idx]); - return; - } - - if (c > ' ' && NotEncoded(c)) - { - result.WriteByte((byte)c); - return; - } - if (c == ' ') - { - result.WriteByte((byte)'+'); - return; - } - if ((c < '0') || - (c < 'A' && c > '9') || - (c > 'Z' && c < 'a') || - (c > 'z')) - { - if (isUnicode && c > 127) - { - result.WriteByte((byte)'%'); - result.WriteByte((byte)'u'); - result.WriteByte((byte)'0'); - result.WriteByte((byte)'0'); - } - else - result.WriteByte((byte)'%'); - - int idx = ((int)c) >> 4; - result.WriteByte((byte)hexChars[idx]); - idx = ((int)c) & 0x0F; - result.WriteByte((byte)hexChars[idx]); - } - else - result.WriteByte((byte)c); - } - - internal static void UrlPathEncodeChar(char c, Stream result) - { - if (c < 33 || c > 126) - { - byte[] bIn = Encoding.UTF8.GetBytes(c.ToString()); - for (int i = 0; i < bIn.Length; i++) - { - result.WriteByte((byte)'%'); - int idx = ((int)bIn[i]) >> 4; - result.WriteByte((byte)hexChars[idx]); - idx = ((int)bIn[i]) & 0x0F; - result.WriteByte((byte)hexChars[idx]); - } - } - else if (c == ' ') - { - result.WriteByte((byte)'%'); - result.WriteByte((byte)'2'); - result.WriteByte((byte)'0'); - } - else - result.WriteByte((byte)c); - } - - static void InitEntities() - { - // Build the hash table of HTML entity references. This list comes - // from the HTML 4.01 W3C recommendation. -#if PocketPC - entities = new Dictionary(StringComparer.Ordinal); -#else - entities = new SortedDictionary(StringComparer.Ordinal); -#endif - entities.Add("nbsp", '\u00A0'); - entities.Add("iexcl", '\u00A1'); - entities.Add("cent", '\u00A2'); - entities.Add("pound", '\u00A3'); - entities.Add("curren", '\u00A4'); - entities.Add("yen", '\u00A5'); - entities.Add("brvbar", '\u00A6'); - entities.Add("sect", '\u00A7'); - entities.Add("uml", '\u00A8'); - entities.Add("copy", '\u00A9'); - entities.Add("ordf", '\u00AA'); - entities.Add("laquo", '\u00AB'); - entities.Add("not", '\u00AC'); - entities.Add("shy", '\u00AD'); - entities.Add("reg", '\u00AE'); - entities.Add("macr", '\u00AF'); - entities.Add("deg", '\u00B0'); - entities.Add("plusmn", '\u00B1'); - entities.Add("sup2", '\u00B2'); - entities.Add("sup3", '\u00B3'); - entities.Add("acute", '\u00B4'); - entities.Add("micro", '\u00B5'); - entities.Add("para", '\u00B6'); - entities.Add("middot", '\u00B7'); - entities.Add("cedil", '\u00B8'); - entities.Add("sup1", '\u00B9'); - entities.Add("ordm", '\u00BA'); - entities.Add("raquo", '\u00BB'); - entities.Add("frac14", '\u00BC'); - entities.Add("frac12", '\u00BD'); - entities.Add("frac34", '\u00BE'); - entities.Add("iquest", '\u00BF'); - entities.Add("Agrave", '\u00C0'); - entities.Add("Aacute", '\u00C1'); - entities.Add("Acirc", '\u00C2'); - entities.Add("Atilde", '\u00C3'); - entities.Add("Auml", '\u00C4'); - entities.Add("Aring", '\u00C5'); - entities.Add("AElig", '\u00C6'); - entities.Add("Ccedil", '\u00C7'); - entities.Add("Egrave", '\u00C8'); - entities.Add("Eacute", '\u00C9'); - entities.Add("Ecirc", '\u00CA'); - entities.Add("Euml", '\u00CB'); - entities.Add("Igrave", '\u00CC'); - entities.Add("Iacute", '\u00CD'); - entities.Add("Icirc", '\u00CE'); - entities.Add("Iuml", '\u00CF'); - entities.Add("ETH", '\u00D0'); - entities.Add("Ntilde", '\u00D1'); - entities.Add("Ograve", '\u00D2'); - entities.Add("Oacute", '\u00D3'); - entities.Add("Ocirc", '\u00D4'); - entities.Add("Otilde", '\u00D5'); - entities.Add("Ouml", '\u00D6'); - entities.Add("times", '\u00D7'); - entities.Add("Oslash", '\u00D8'); - entities.Add("Ugrave", '\u00D9'); - entities.Add("Uacute", '\u00DA'); - entities.Add("Ucirc", '\u00DB'); - entities.Add("Uuml", '\u00DC'); - entities.Add("Yacute", '\u00DD'); - entities.Add("THORN", '\u00DE'); - entities.Add("szlig", '\u00DF'); - entities.Add("agrave", '\u00E0'); - entities.Add("aacute", '\u00E1'); - entities.Add("acirc", '\u00E2'); - entities.Add("atilde", '\u00E3'); - entities.Add("auml", '\u00E4'); - entities.Add("aring", '\u00E5'); - entities.Add("aelig", '\u00E6'); - entities.Add("ccedil", '\u00E7'); - entities.Add("egrave", '\u00E8'); - entities.Add("eacute", '\u00E9'); - entities.Add("ecirc", '\u00EA'); - entities.Add("euml", '\u00EB'); - entities.Add("igrave", '\u00EC'); - entities.Add("iacute", '\u00ED'); - entities.Add("icirc", '\u00EE'); - entities.Add("iuml", '\u00EF'); - entities.Add("eth", '\u00F0'); - entities.Add("ntilde", '\u00F1'); - entities.Add("ograve", '\u00F2'); - entities.Add("oacute", '\u00F3'); - entities.Add("ocirc", '\u00F4'); - entities.Add("otilde", '\u00F5'); - entities.Add("ouml", '\u00F6'); - entities.Add("divide", '\u00F7'); - entities.Add("oslash", '\u00F8'); - entities.Add("ugrave", '\u00F9'); - entities.Add("uacute", '\u00FA'); - entities.Add("ucirc", '\u00FB'); - entities.Add("uuml", '\u00FC'); - entities.Add("yacute", '\u00FD'); - entities.Add("thorn", '\u00FE'); - entities.Add("yuml", '\u00FF'); - entities.Add("fnof", '\u0192'); - entities.Add("Alpha", '\u0391'); - entities.Add("Beta", '\u0392'); - entities.Add("Gamma", '\u0393'); - entities.Add("Delta", '\u0394'); - entities.Add("Epsilon", '\u0395'); - entities.Add("Zeta", '\u0396'); - entities.Add("Eta", '\u0397'); - entities.Add("Theta", '\u0398'); - entities.Add("Iota", '\u0399'); - entities.Add("Kappa", '\u039A'); - entities.Add("Lambda", '\u039B'); - entities.Add("Mu", '\u039C'); - entities.Add("Nu", '\u039D'); - entities.Add("Xi", '\u039E'); - entities.Add("Omicron", '\u039F'); - entities.Add("Pi", '\u03A0'); - entities.Add("Rho", '\u03A1'); - entities.Add("Sigma", '\u03A3'); - entities.Add("Tau", '\u03A4'); - entities.Add("Upsilon", '\u03A5'); - entities.Add("Phi", '\u03A6'); - entities.Add("Chi", '\u03A7'); - entities.Add("Psi", '\u03A8'); - entities.Add("Omega", '\u03A9'); - entities.Add("alpha", '\u03B1'); - entities.Add("beta", '\u03B2'); - entities.Add("gamma", '\u03B3'); - entities.Add("delta", '\u03B4'); - entities.Add("epsilon", '\u03B5'); - entities.Add("zeta", '\u03B6'); - entities.Add("eta", '\u03B7'); - entities.Add("theta", '\u03B8'); - entities.Add("iota", '\u03B9'); - entities.Add("kappa", '\u03BA'); - entities.Add("lambda", '\u03BB'); - entities.Add("mu", '\u03BC'); - entities.Add("nu", '\u03BD'); - entities.Add("xi", '\u03BE'); - entities.Add("omicron", '\u03BF'); - entities.Add("pi", '\u03C0'); - entities.Add("rho", '\u03C1'); - entities.Add("sigmaf", '\u03C2'); - entities.Add("sigma", '\u03C3'); - entities.Add("tau", '\u03C4'); - entities.Add("upsilon", '\u03C5'); - entities.Add("phi", '\u03C6'); - entities.Add("chi", '\u03C7'); - entities.Add("psi", '\u03C8'); - entities.Add("omega", '\u03C9'); - entities.Add("thetasym", '\u03D1'); - entities.Add("upsih", '\u03D2'); - entities.Add("piv", '\u03D6'); - entities.Add("bull", '\u2022'); - entities.Add("hellip", '\u2026'); - entities.Add("prime", '\u2032'); - entities.Add("Prime", '\u2033'); - entities.Add("oline", '\u203E'); - entities.Add("frasl", '\u2044'); - entities.Add("weierp", '\u2118'); - entities.Add("image", '\u2111'); - entities.Add("real", '\u211C'); - entities.Add("trade", '\u2122'); - entities.Add("alefsym", '\u2135'); - entities.Add("larr", '\u2190'); - entities.Add("uarr", '\u2191'); - entities.Add("rarr", '\u2192'); - entities.Add("darr", '\u2193'); - entities.Add("harr", '\u2194'); - entities.Add("crarr", '\u21B5'); - entities.Add("lArr", '\u21D0'); - entities.Add("uArr", '\u21D1'); - entities.Add("rArr", '\u21D2'); - entities.Add("dArr", '\u21D3'); - entities.Add("hArr", '\u21D4'); - entities.Add("forall", '\u2200'); - entities.Add("part", '\u2202'); - entities.Add("exist", '\u2203'); - entities.Add("empty", '\u2205'); - entities.Add("nabla", '\u2207'); - entities.Add("isin", '\u2208'); - entities.Add("notin", '\u2209'); - entities.Add("ni", '\u220B'); - entities.Add("prod", '\u220F'); - entities.Add("sum", '\u2211'); - entities.Add("minus", '\u2212'); - entities.Add("lowast", '\u2217'); - entities.Add("radic", '\u221A'); - entities.Add("prop", '\u221D'); - entities.Add("infin", '\u221E'); - entities.Add("ang", '\u2220'); - entities.Add("and", '\u2227'); - entities.Add("or", '\u2228'); - entities.Add("cap", '\u2229'); - entities.Add("cup", '\u222A'); - entities.Add("int", '\u222B'); - entities.Add("there4", '\u2234'); - entities.Add("sim", '\u223C'); - entities.Add("cong", '\u2245'); - entities.Add("asymp", '\u2248'); - entities.Add("ne", '\u2260'); - entities.Add("equiv", '\u2261'); - entities.Add("le", '\u2264'); - entities.Add("ge", '\u2265'); - entities.Add("sub", '\u2282'); - entities.Add("sup", '\u2283'); - entities.Add("nsub", '\u2284'); - entities.Add("sube", '\u2286'); - entities.Add("supe", '\u2287'); - entities.Add("oplus", '\u2295'); - entities.Add("otimes", '\u2297'); - entities.Add("perp", '\u22A5'); - entities.Add("sdot", '\u22C5'); - entities.Add("lceil", '\u2308'); - entities.Add("rceil", '\u2309'); - entities.Add("lfloor", '\u230A'); - entities.Add("rfloor", '\u230B'); - entities.Add("lang", '\u2329'); - entities.Add("rang", '\u232A'); - entities.Add("loz", '\u25CA'); - entities.Add("spades", '\u2660'); - entities.Add("clubs", '\u2663'); - entities.Add("hearts", '\u2665'); - entities.Add("diams", '\u2666'); - entities.Add("quot", '\u0022'); - entities.Add("amp", '\u0026'); - entities.Add("lt", '\u003C'); - entities.Add("gt", '\u003E'); - entities.Add("OElig", '\u0152'); - entities.Add("oelig", '\u0153'); - entities.Add("Scaron", '\u0160'); - entities.Add("scaron", '\u0161'); - entities.Add("Yuml", '\u0178'); - entities.Add("circ", '\u02C6'); - entities.Add("tilde", '\u02DC'); - entities.Add("ensp", '\u2002'); - entities.Add("emsp", '\u2003'); - entities.Add("thinsp", '\u2009'); - entities.Add("zwnj", '\u200C'); - entities.Add("zwj", '\u200D'); - entities.Add("lrm", '\u200E'); - entities.Add("rlm", '\u200F'); - entities.Add("ndash", '\u2013'); - entities.Add("mdash", '\u2014'); - entities.Add("lsquo", '\u2018'); - entities.Add("rsquo", '\u2019'); - entities.Add("sbquo", '\u201A'); - entities.Add("ldquo", '\u201C'); - entities.Add("rdquo", '\u201D'); - entities.Add("bdquo", '\u201E'); - entities.Add("dagger", '\u2020'); - entities.Add("Dagger", '\u2021'); - entities.Add("permil", '\u2030'); - entities.Add("lsaquo", '\u2039'); - entities.Add("rsaquo", '\u203A'); - entities.Add("euro", '\u20AC'); - } - } -} \ No newline at end of file diff --git a/RestSharp/Extensions/MonoHttp/HttpUtility.cs b/RestSharp/Extensions/MonoHttp/HttpUtility.cs deleted file mode 100644 index 7fe8f2b2d..000000000 --- a/RestSharp/Extensions/MonoHttp/HttpUtility.cs +++ /dev/null @@ -1,767 +0,0 @@ -// -// System.Web.HttpUtility -// -// Authors: -// Patrik Torstensson (Patrik.Torstensson@labs2.com) -// Wictor Wilén (decode/encode functions) (wictor@ibizkit.se) -// Tim Coleman (tim@timcoleman.com) -// Gonzalo Paniagua Javier (gonzalo@ximian.com) -// -// Copyright (C) 2005-2010 Novell, Inc (http://www.novell.com) -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Globalization; -using System.IO; -using System.Security.Permissions; -using System.Text; - -namespace RestSharp.Contrib -{ - -//#if !MONOTOUCH -// // CAS - no InheritanceDemand here as the class is sealed -// [AspNetHostingPermission(SecurityAction.LinkDemand, Level = AspNetHostingPermissionLevel.Minimal)] -//#endif - public sealed class HttpUtility - { - sealed class HttpQSCollection : NameValueCollection - { - public override string ToString() - { - int count = Count; - if (count == 0) - return ""; - StringBuilder sb = new StringBuilder(); - string[] keys = AllKeys; - for (int i = 0; i < count; i++) - { - sb.AppendFormat("{0}={1}&", keys[i], this[keys[i]]); - } - if (sb.Length > 0) - sb.Length--; - return sb.ToString(); - } - } - - #region Constructors - - public HttpUtility() - { - } - - #endregion // Constructors - - #region Methods - - public static void HtmlAttributeEncode(string s, TextWriter output) - { - if (output == null) - { -#if NET_4_0 - throw new ArgumentNullException ("output"); -#else - throw new NullReferenceException(".NET emulation"); -#endif - } -#if NET_4_0 - HttpEncoder.Current.HtmlAttributeEncode (s, output); -#else - output.Write(HttpEncoder.HtmlAttributeEncode(s)); -#endif - } - - public static string HtmlAttributeEncode(string s) - { -#if NET_4_0 - if (s == null) - return null; - - using (var sw = new StringWriter ()) { - HttpEncoder.Current.HtmlAttributeEncode (s, sw); - return sw.ToString (); - } -#else - return HttpEncoder.HtmlAttributeEncode(s); -#endif - } - - public static string UrlDecode(string str) - { - return UrlDecode(str, Encoding.UTF8); - } - - static char[] GetChars(MemoryStream b, Encoding e) - { - return e.GetChars(b.GetBuffer(), 0, (int)b.Length); - } - - static void WriteCharBytes(IList buf, char ch, Encoding e) - { - if (ch > 255) - { - foreach (byte b in e.GetBytes(new char[] { ch })) - buf.Add(b); - } - else - buf.Add((byte)ch); - } - - public static string UrlDecode(string s, Encoding e) - { - if (null == s) - return null; - - if (s.IndexOf('%') == -1 && s.IndexOf('+') == -1) - return s; - - if (e == null) - e = Encoding.UTF8; - - long len = s.Length; - var bytes = new List(); - int xchar; - char ch; - - for (int i = 0; i < len; i++) - { - ch = s[i]; - if (ch == '%' && i + 2 < len && s[i + 1] != '%') - { - if (s[i + 1] == 'u' && i + 5 < len) - { - // unicode hex sequence - xchar = GetChar(s, i + 2, 4); - if (xchar != -1) - { - WriteCharBytes(bytes, (char)xchar, e); - i += 5; - } - else - WriteCharBytes(bytes, '%', e); - } - else if ((xchar = GetChar(s, i + 1, 2)) != -1) - { - WriteCharBytes(bytes, (char)xchar, e); - i += 2; - } - else - { - WriteCharBytes(bytes, '%', e); - } - continue; - } - - if (ch == '+') - WriteCharBytes(bytes, ' ', e); - else - WriteCharBytes(bytes, ch, e); - } - - byte[] buf = bytes.ToArray(); - bytes = null; - return e.GetString(buf, 0, buf.Length); - - } - - public static string UrlDecode(byte[] bytes, Encoding e) - { - if (bytes == null) - return null; - - return UrlDecode(bytes, 0, bytes.Length, e); - } - - static int GetInt(byte b) - { - char c = (char)b; - if (c >= '0' && c <= '9') - return c - '0'; - - if (c >= 'a' && c <= 'f') - return c - 'a' + 10; - - if (c >= 'A' && c <= 'F') - return c - 'A' + 10; - - return -1; - } - - static int GetChar(byte[] bytes, int offset, int length) - { - int value = 0; - int end = length + offset; - for (int i = offset; i < end; i++) - { - int current = GetInt(bytes[i]); - if (current == -1) - return -1; - value = (value << 4) + current; - } - - return value; - } - - static int GetChar(string str, int offset, int length) - { - int val = 0; - int end = length + offset; - for (int i = offset; i < end; i++) - { - char c = str[i]; - if (c > 127) - return -1; - - int current = GetInt((byte)c); - if (current == -1) - return -1; - val = (val << 4) + current; - } - - return val; - } - - public static string UrlDecode(byte[] bytes, int offset, int count, Encoding e) - { - if (bytes == null) - return null; - if (count == 0) - return String.Empty; - - if (bytes == null) - throw new ArgumentNullException("bytes"); - - if (offset < 0 || offset > bytes.Length) - throw new ArgumentOutOfRangeException("offset"); - - if (count < 0 || offset + count > bytes.Length) - throw new ArgumentOutOfRangeException("count"); - - StringBuilder output = new StringBuilder(); - MemoryStream acc = new MemoryStream(); - - int end = count + offset; - int xchar; - for (int i = offset; i < end; i++) - { - if (bytes[i] == '%' && i + 2 < count && bytes[i + 1] != '%') - { - if (bytes[i + 1] == (byte)'u' && i + 5 < end) - { - if (acc.Length > 0) - { - output.Append(GetChars(acc, e)); - acc.SetLength(0); - } - xchar = GetChar(bytes, i + 2, 4); - if (xchar != -1) - { - output.Append((char)xchar); - i += 5; - continue; - } - } - else if ((xchar = GetChar(bytes, i + 1, 2)) != -1) - { - acc.WriteByte((byte)xchar); - i += 2; - continue; - } - } - - if (acc.Length > 0) - { - output.Append(GetChars(acc, e)); - acc.SetLength(0); - } - - if (bytes[i] == '+') - { - output.Append(' '); - } - else - { - output.Append((char)bytes[i]); - } - } - - if (acc.Length > 0) - { - output.Append(GetChars(acc, e)); - } - - acc = null; - return output.ToString(); - } - - public static byte[] UrlDecodeToBytes(byte[] bytes) - { - if (bytes == null) - return null; - - return UrlDecodeToBytes(bytes, 0, bytes.Length); - } - - public static byte[] UrlDecodeToBytes(string str) - { - return UrlDecodeToBytes(str, Encoding.UTF8); - } - - public static byte[] UrlDecodeToBytes(string str, Encoding e) - { - if (str == null) - return null; - - if (e == null) - throw new ArgumentNullException("e"); - - return UrlDecodeToBytes(e.GetBytes(str)); - } - - public static byte[] UrlDecodeToBytes(byte[] bytes, int offset, int count) - { - if (bytes == null) - return null; - if (count == 0) - return new byte[0]; - - int len = bytes.Length; - if (offset < 0 || offset >= len) - throw new ArgumentOutOfRangeException("offset"); - - if (count < 0 || offset > len - count) - throw new ArgumentOutOfRangeException("count"); - - MemoryStream result = new MemoryStream(); - int end = offset + count; - for (int i = offset; i < end; i++) - { - char c = (char)bytes[i]; - if (c == '+') - { - c = ' '; - } - else if (c == '%' && i < end - 2) - { - int xchar = GetChar(bytes, i + 1, 2); - if (xchar != -1) - { - c = (char)xchar; - i += 2; - } - } - result.WriteByte((byte)c); - } - - return result.ToArray(); - } - - public static string UrlEncode(string str) - { - return UrlEncode(str, Encoding.UTF8); - } - - public static string UrlEncode(string s, Encoding Enc) - { - if (s == null) - return null; - - if (s == String.Empty) - return String.Empty; - - bool needEncode = false; - int len = s.Length; - for (int i = 0; i < len; i++) - { - char c = s[i]; - if ((c < '0') || (c < 'A' && c > '9') || (c > 'Z' && c < 'a') || (c > 'z')) - { - if (HttpEncoder.NotEncoded(c)) - continue; - - needEncode = true; - break; - } - } - - if (!needEncode) - return s; - - // avoided GetByteCount call - byte[] bytes = new byte[Enc.GetMaxByteCount(s.Length)]; - int realLen = Enc.GetBytes(s, 0, s.Length, bytes, 0); - byte[] r = UrlEncodeToBytes(bytes, 0, realLen); - return Encoding.ASCII.GetString(r, 0, r.Length); - } - - public static string UrlEncode(byte[] bytes) - { - if (bytes == null) - return null; - - if (bytes.Length == 0) - return String.Empty; - byte[] r = UrlEncodeToBytes(bytes, 0, bytes.Length); - return Encoding.ASCII.GetString(r, 0, r.Length); - } - - public static string UrlEncode(byte[] bytes, int offset, int count) - { - if (bytes == null) - return null; - - if (bytes.Length == 0) - return String.Empty; - byte[] r = UrlEncodeToBytes(bytes, offset, count); - return Encoding.ASCII.GetString(r, 0, r.Length); - } - - public static byte[] UrlEncodeToBytes(string str) - { - return UrlEncodeToBytes(str, Encoding.UTF8); - } - - public static byte[] UrlEncodeToBytes(string str, Encoding e) - { - if (str == null) - return null; - - if (str.Length == 0) - return new byte[0]; - - byte[] bytes = e.GetBytes(str); - return UrlEncodeToBytes(bytes, 0, bytes.Length); - } - - public static byte[] UrlEncodeToBytes(byte[] bytes) - { - if (bytes == null) - return null; - - if (bytes.Length == 0) - return new byte[0]; - - return UrlEncodeToBytes(bytes, 0, bytes.Length); - } - - public static byte[] UrlEncodeToBytes(byte[] bytes, int offset, int count) - { - if (bytes == null) - return null; -#if NET_4_0 - return HttpEncoder.Current.UrlEncode (bytes, offset, count); -#else - return HttpEncoder.UrlEncodeToBytes(bytes, offset, count); -#endif - } - - public static string UrlEncodeUnicode(string str) - { - if (str == null) - return null; - byte[] r = UrlEncodeUnicodeToBytes(str); - return Encoding.ASCII.GetString(r, 0, r.Length); - } - - public static byte[] UrlEncodeUnicodeToBytes(string str) - { - if (str == null) - return null; - - if (str.Length == 0) - return new byte[0]; - - MemoryStream result = new MemoryStream(str.Length); - foreach (char c in str) - { - HttpEncoder.UrlEncodeChar(c, result, true); - } - return result.ToArray(); - } - - /// - /// Decodes an HTML-encoded string and returns the decoded string. - /// - /// The HTML string to decode. - /// The decoded text. - public static string HtmlDecode(string s) - { -#if NET_4_0 - if (s == null) - return null; - - using (var sw = new StringWriter ()) { - HttpEncoder.Current.HtmlDecode (s, sw); - return sw.ToString (); - } -#else - return HttpEncoder.HtmlDecode(s); -#endif - } - - /// - /// Decodes an HTML-encoded string and sends the resulting output to a TextWriter output stream. - /// - /// The HTML string to decode - /// The TextWriter output stream containing the decoded string. - public static void HtmlDecode(string s, TextWriter output) - { - if (output == null) - { -#if NET_4_0 - throw new ArgumentNullException ("output"); -#else - throw new NullReferenceException(".NET emulation"); -#endif - } - - if (!String.IsNullOrEmpty(s)) - { -#if NET_4_0 - HttpEncoder.Current.HtmlDecode (s, output); -#else - output.Write(HttpEncoder.HtmlDecode(s)); -#endif - } - } - - public static string HtmlEncode(string s) - { -#if NET_4_0 - if (s == null) - return null; - - using (var sw = new StringWriter ()) { - HttpEncoder.Current.HtmlEncode (s, sw); - return sw.ToString (); - } -#else - return HttpEncoder.HtmlEncode(s); -#endif - } - - /// - /// HTML-encodes a string and sends the resulting output to a TextWriter output stream. - /// - /// The string to encode. - /// The TextWriter output stream containing the encoded string. - public static void HtmlEncode(string s, TextWriter output) - { - if (output == null) - { -#if NET_4_0 - throw new ArgumentNullException ("output"); -#else - throw new NullReferenceException(".NET emulation"); -#endif - } - - if (!String.IsNullOrEmpty(s)) - { -#if NET_4_0 - HttpEncoder.Current.HtmlEncode (s, output); -#else - output.Write(HttpEncoder.HtmlEncode(s)); -#endif - } - } -#if NET_4_0 - public static string HtmlEncode (object value) - { - if (value == null) - return null; - - IHtmlString htmlString = value as IHtmlString; - if (htmlString != null) - return htmlString.ToHtmlString (); - - return HtmlEncode (value.ToString ()); - } - - public static string JavaScriptStringEncode (string value) - { - return JavaScriptStringEncode (value, false); - } - - public static string JavaScriptStringEncode (string value, bool addDoubleQuotes) - { - if (String.IsNullOrEmpty (value)) - return addDoubleQuotes ? "\"\"" : String.Empty; - - int len = value.Length; - bool needEncode = false; - char c; - for (int i = 0; i < len; i++) { - c = value [i]; - - if (c >= 0 && c <= 31 || c == 34 || c == 39 || c == 60 || c == 62 || c == 92) { - needEncode = true; - break; - } - } - - if (!needEncode) - return addDoubleQuotes ? "\"" + value + "\"" : value; - - var sb = new StringBuilder (); - if (addDoubleQuotes) - sb.Append ('"'); - - for (int i = 0; i < len; i++) { - c = value [i]; - if (c >= 0 && c <= 7 || c == 11 || c >= 14 && c <= 31 || c == 39 || c == 60 || c == 62) - sb.AppendFormat ("\\u{0:x4}", (int)c); - else switch ((int)c) { - case 8: - sb.Append ("\\b"); - break; - - case 9: - sb.Append ("\\t"); - break; - - case 10: - sb.Append ("\\n"); - break; - - case 12: - sb.Append ("\\f"); - break; - - case 13: - sb.Append ("\\r"); - break; - - case 34: - sb.Append ("\\\""); - break; - - case 92: - sb.Append ("\\\\"); - break; - - default: - sb.Append (c); - break; - } - } - - if (addDoubleQuotes) - sb.Append ('"'); - - return sb.ToString (); - } -#endif - public static string UrlPathEncode(string s) - { -#if NET_4_0 - return HttpEncoder.Current.UrlPathEncode (s); -#else - return HttpEncoder.UrlPathEncode(s); -#endif - } - - public static NameValueCollection ParseQueryString(string query) - { - return ParseQueryString(query, Encoding.UTF8); - } - - public static NameValueCollection ParseQueryString(string query, Encoding encoding) - { - if (query == null) - throw new ArgumentNullException("query"); - if (encoding == null) - throw new ArgumentNullException("encoding"); - if (query.Length == 0 || (query.Length == 1 && query[0] == '?')) - return new NameValueCollection(); - if (query[0] == '?') - query = query.Substring(1); - - NameValueCollection result = new HttpQSCollection(); - ParseQueryString(query, encoding, result); - return result; - } - - internal static void ParseQueryString(string query, Encoding encoding, NameValueCollection result) - { - if (query.Length == 0) - return; - - string decoded = HtmlDecode(query); - int decodedLength = decoded.Length; - int namePos = 0; - bool first = true; - while (namePos <= decodedLength) - { - int valuePos = -1, valueEnd = -1; - for (int q = namePos; q < decodedLength; q++) - { - if (valuePos == -1 && decoded[q] == '=') - { - valuePos = q + 1; - } - else if (decoded[q] == '&') - { - valueEnd = q; - break; - } - } - - if (first) - { - first = false; - if (decoded[namePos] == '?') - namePos++; - } - - string name, value; - if (valuePos == -1) - { - name = null; - valuePos = namePos; - } - else - { - name = UrlDecode(decoded.Substring(namePos, valuePos - namePos - 1), encoding); - } - if (valueEnd < 0) - { - namePos = -1; - valueEnd = decoded.Length; - } - else - { - namePos = valueEnd + 1; - } - value = UrlDecode(decoded.Substring(valuePos, valueEnd - valuePos), encoding); - - result.Add(name, value); - if (namePos == -1) - break; - } - } - #endregion // Methods - } -} diff --git a/RestSharp/Extensions/ReflectionExtensions.cs b/RestSharp/Extensions/ReflectionExtensions.cs deleted file mode 100644 index 82fa61625..000000000 --- a/RestSharp/Extensions/ReflectionExtensions.cs +++ /dev/null @@ -1,114 +0,0 @@ -#region License -// Copyright 2010 John Sheehan -// -// 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. -#endregion - -using System; -using System.Globalization; -using System.Linq; -using System.Reflection; - -namespace RestSharp.Extensions -{ - /// - /// Reflection extensions - /// - public static class ReflectionExtensions - { - /// - /// Retrieve an attribute from a member (property) - /// - /// Type of attribute to retrieve - /// Member to retrieve attribute from - /// - public static T GetAttribute(this MemberInfo prop) where T : Attribute { - return Attribute.GetCustomAttribute(prop, typeof(T)) as T; - } - - /// - /// Retrieve an attribute from a type - /// - /// Type of attribute to retrieve - /// Type to retrieve attribute from - /// - public static T GetAttribute(this Type type) where T : Attribute { - return Attribute.GetCustomAttribute(type, typeof(T)) as T; - } - - /// - /// Checks a type to see if it derives from a raw generic (e.g. List[[]]) - /// - /// - /// - /// - public static bool IsSubclassOfRawGeneric(this Type toCheck, Type generic) { - while (toCheck != typeof(object)) { - var cur = toCheck.IsGenericType ? toCheck.GetGenericTypeDefinition() : toCheck; - if (generic == cur) { - return true; - } - toCheck = toCheck.BaseType; - } - return false; - } - - public static object ChangeType(this object source, Type newType) - { -#if FRAMEWORK && !PocketPC - return Convert.ChangeType(source, newType); -#else - return Convert.ChangeType(source, newType, null); -#endif - } - - public static object ChangeType(this object source, Type newType, CultureInfo culture) - { -#if FRAMEWORK || SILVERLIGHT || WINDOWS_PHONE - return Convert.ChangeType(source, newType, culture); -#else - return Convert.ChangeType(source, newType, null); -#endif - } - - /// - /// Find a value from a System.Enum by trying several possible variants - /// of the string value of the enum. - /// - /// Type of enum - /// Value for which to search - /// The culture used to calculate the name variants - /// - public static object FindEnumValue(this Type type, string value, CultureInfo culture) - { -#if FRAMEWORK && !PocketPC - var ret = Enum.GetValues( type ) - .Cast() - .FirstOrDefault(v => v.ToString().GetNameVariants(culture).Contains(value, StringComparer.Create(culture, true))); - - if (ret == null) - { - var enumValueAsUnderlyingType = Convert.ChangeType(value, Enum.GetUnderlyingType(type), culture); - if (enumValueAsUnderlyingType != null && Enum.IsDefined(type, enumValueAsUnderlyingType)) - { - ret = (Enum) Enum.ToObject(type, enumValueAsUnderlyingType); - } - } - - return ret; -#else - return Enum.Parse(type, value, true); -#endif - } - } -} diff --git a/RestSharp/Extensions/ResponseExtensions.cs b/RestSharp/Extensions/ResponseExtensions.cs deleted file mode 100644 index 89df2c6bf..000000000 --- a/RestSharp/Extensions/ResponseExtensions.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace RestSharp.Extensions -{ - public static class ResponseExtensions - { - public static IRestResponse toAsyncResponse(this IRestResponse response) - { - return new RestResponse - { - ContentEncoding = response.ContentEncoding, - ContentLength = response.ContentLength, - ContentType = response.ContentType, - Cookies = response.Cookies, - ErrorException = response.ErrorException, - ErrorMessage = response.ErrorMessage, - Headers = response.Headers, - RawBytes = response.RawBytes, - ResponseStatus = response.ResponseStatus, - ResponseUri = response.ResponseUri, - Server = response.Server, - StatusCode = response.StatusCode, - StatusDescription = response.StatusDescription - }; - } - } -} diff --git a/RestSharp/Extensions/StringExtensions.cs b/RestSharp/Extensions/StringExtensions.cs deleted file mode 100644 index 92dfc35cc..000000000 --- a/RestSharp/Extensions/StringExtensions.cs +++ /dev/null @@ -1,396 +0,0 @@ -#region License -// Copyright 2010 John Sheehan -// -// 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. -#endregion - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Text; -using System.Text.RegularExpressions; -using System.Globalization; - -#if SILVERLIGHT -using System.Windows.Browser; -#endif - -#if WINDOWS_PHONE -#endif - -#if FRAMEWORK || MONOTOUCH || MONODROID -using RestSharp.Contrib; -#endif - - -namespace RestSharp.Extensions -{ - public static class StringExtensions - { -#if !PocketPC - public static string UrlDecode(this string input) - { - return HttpUtility.UrlDecode(input); - } -#endif - - /// - /// Uses Uri.EscapeDataString() based on recommendations on MSDN - /// http://blogs.msdn.com/b/yangxind/archive/2006/11/09/don-t-use-net-system-uri-unescapedatastring-in-url-decoding.aspx - /// - public static string UrlEncode(this string input) - { - const int maxLength = 32766; - if (input == null) - throw new ArgumentNullException("input"); - - if (input.Length <= maxLength) - return Uri.EscapeDataString(input); - - StringBuilder sb = new StringBuilder(input.Length * 2); - int index = 0; - while (index < input.Length) - { - int length = Math.Min(input.Length - index, maxLength); - string subString = input.Substring(index, length); - sb.Append(Uri.EscapeDataString(subString)); - index += subString.Length; - } - - return sb.ToString(); - } - -#if !PocketPC - public static string HtmlDecode(this string input) - { - return HttpUtility.HtmlDecode(input); - } - - public static string HtmlEncode(this string input) - { - return HttpUtility.HtmlEncode(input); - } -#endif - -#if FRAMEWORK - public static string HtmlAttributeEncode(this string input) - { - return HttpUtility.HtmlAttributeEncode(input); - } -#endif - - /// - /// Check that a string is not null or empty - /// - /// String to check - /// bool - public static bool HasValue(this string input) - { - return !string.IsNullOrEmpty(input); - } - - /// - /// Remove underscores from a string - /// - /// String to process - /// string - public static string RemoveUnderscoresAndDashes(this string input) - { - return input.Replace("_", "").Replace("-", ""); // avoiding regex - } - - /// - /// Parses most common JSON date formats - /// - /// JSON value to parse - /// DateTime - public static DateTime ParseJsonDate(this string input, CultureInfo culture) - { - input = input.Replace("\n", ""); - input = input.Replace("\r", ""); - - input = input.RemoveSurroundingQuotes(); - - long? unix = null; - try { - unix = Int64.Parse(input); - } catch (Exception) { }; - if (unix.HasValue) - { - var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); - return epoch.AddSeconds(unix.Value); - } - - if (input.Contains("/Date(")) - { - return ExtractDate(input, @"\\?/Date\((-?\d+)(-|\+)?([0-9]{4})?\)\\?/", culture); - } - - if (input.Contains("new Date(")) - { - input = input.Replace(" ", ""); - // because all whitespace is removed, match against newDate( instead of new Date( - return ExtractDate(input, @"newDate\((-?\d+)*\)", culture); - } - - return ParseFormattedDate(input, culture); - } - - /// - /// Remove leading and trailing " from a string - /// - /// String to parse - /// String - public static string RemoveSurroundingQuotes(this string input) - { - if (input.StartsWith("\"") && input.EndsWith("\"")) - { - // remove leading/trailing quotes - input = input.Substring(1, input.Length - 2); - } - return input; - } - - private static DateTime ParseFormattedDate(string input, CultureInfo culture) - { - var formats = new[] { - "u", - "s", - "yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'", - "yyyy-MM-ddTHH:mm:ssZ", - "yyyy-MM-dd HH:mm:ssZ", - "yyyy-MM-ddTHH:mm:ss", - "yyyy-MM-ddTHH:mm:sszzzzzz", - "M/d/yyyy h:mm:ss tt" // default format for invariant culture - }; - -#if PocketPC - foreach (string format in formats) { - try { - return DateTime.ParseExact(input, format, culture); - } catch (Exception) { - } - } - try { - return DateTime.Parse(input, culture); - } catch (Exception) { - } -#else - DateTime date; - if (DateTime.TryParseExact(input, formats, culture, DateTimeStyles.None, out date)) - { - return date; - } - if (DateTime.TryParse(input, culture, DateTimeStyles.None, out date)) - { - return date; - } -#endif - - return default(DateTime); - } - - private static DateTime ExtractDate(string input, string pattern, CultureInfo culture) - { - DateTime dt = DateTime.MinValue; - var regex = new Regex(pattern); - if (regex.IsMatch(input)) - { - var matches = regex.Matches(input); - var match = matches[0]; - var ms = Convert.ToInt64(match.Groups[1].Value); - var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); - dt = epoch.AddMilliseconds(ms); - - // adjust if time zone modifier present - if (match.Groups.Count > 2 && !String.IsNullOrEmpty(match.Groups[3].Value)) - { - var mod = DateTime.ParseExact(match.Groups[3].Value, "HHmm", culture); - if (match.Groups[2].Value == "+") - { - dt = dt.Add(mod.TimeOfDay); - } - else - { - dt = dt.Subtract(mod.TimeOfDay); - } - } - - } - return dt; - } - - /// - /// Checks a string to see if it matches a regex - /// - /// String to check - /// Pattern to match - /// bool - public static bool Matches(this string input, string pattern) - { - return Regex.IsMatch(input, pattern); - } - - /// - /// Converts a string to pascal case - /// - /// String to convert - /// string - public static string ToPascalCase(this string lowercaseAndUnderscoredWord, CultureInfo culture) - { - return ToPascalCase(lowercaseAndUnderscoredWord, true, culture); - } - - /// - /// Converts a string to pascal case with the option to remove underscores - /// - /// String to convert - /// Option to remove underscores - /// - public static string ToPascalCase(this string text, bool removeUnderscores, CultureInfo culture) - { - if (String.IsNullOrEmpty(text)) - return text; - - text = text.Replace("_", " "); - string joinString = removeUnderscores ? String.Empty : "_"; - string[] words = text.Split(' '); - if (words.Length > 1 || words[0].IsUpperCase()) - { - for (int i = 0; i < words.Length; i++) - { - if (words[i].Length > 0) - { - string word = words[i]; - string restOfWord = word.Substring(1); - - if (restOfWord.IsUpperCase()) - restOfWord = restOfWord.ToLower(culture); - - char firstChar = char.ToUpper(word[0], culture); - words[i] = String.Concat(firstChar, restOfWord); - } - } - return String.Join(joinString, words); - } - return String.Concat(words[0].Substring(0, 1).ToUpper(culture), words[0].Substring(1)); - } - - /// - /// Converts a string to camel case - /// - /// String to convert - /// String - public static string ToCamelCase(this string lowercaseAndUnderscoredWord, CultureInfo culture) - { - return MakeInitialLowerCase(ToPascalCase(lowercaseAndUnderscoredWord, culture)); - } - - /// - /// Convert the first letter of a string to lower case - /// - /// String to convert - /// string - public static string MakeInitialLowerCase(this string word) - { - return String.Concat(word.Substring(0, 1).ToLower(), word.Substring(1)); - } - - /// - /// Checks to see if a string is all uppper case - /// - /// String to check - /// bool - public static bool IsUpperCase(this string inputString) - { - return Regex.IsMatch(inputString, @"^[A-Z]+$"); - } - - /// - /// Add underscores to a pascal-cased string - /// - /// String to convert - /// string - public static string AddUnderscores(this string pascalCasedWord) - { - return - Regex.Replace( - Regex.Replace(Regex.Replace(pascalCasedWord, @"([A-Z]+)([A-Z][a-z])", "$1_$2"), @"([a-z\d])([A-Z])", - "$1_$2"), @"[-\s]", "_"); - } - - /// - /// Add dashes to a pascal-cased string - /// - /// String to convert - /// string - public static string AddDashes(this string pascalCasedWord) - { - return - Regex.Replace( - Regex.Replace( - Regex.Replace(pascalCasedWord, @"([A-Z]+)([A-Z][a-z])", "$1-$2"), - @"([a-z\d])([A-Z])", - "$1-$2"), @"[\s]", "-"); - } - - /// - /// Add an undescore prefix to a pascasl-cased string - /// - /// - /// - public static string AddUnderscorePrefix(this string pascalCasedWord) - { - return string.Format("_{0}", pascalCasedWord); - } - - /// - /// Return possible variants of a name for name matching. - /// - /// String to convert - /// The culture to use for conversion - /// IEnumerable<string> - public static IEnumerable GetNameVariants(this string name, CultureInfo culture) - { - if (String.IsNullOrEmpty(name)) - yield break; - - yield return name; - - // try camel cased name - yield return name.ToCamelCase(culture); - - // try lower cased name - yield return name.ToLower(culture); - - // try name with underscores - yield return name.AddUnderscores(); - - // try name with underscores with lower case - yield return name.AddUnderscores().ToLower(culture); - - // try name with dashes - yield return name.AddDashes(); - - // try name with dashes with lower case - yield return name.AddDashes().ToLower(culture); - - // try name with underscore prefix - yield return name.AddUnderscorePrefix(); - - // try name with underscore prefix, using camel case - yield return name.ToCamelCase(culture).AddUnderscorePrefix(); - } - } -} diff --git a/RestSharp/Extensions/XmlExtensions.cs b/RestSharp/Extensions/XmlExtensions.cs deleted file mode 100644 index e35cce9d9..000000000 --- a/RestSharp/Extensions/XmlExtensions.cs +++ /dev/null @@ -1,41 +0,0 @@ -#region License -// Copyright 2010 John Sheehan -// -// 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. -#endregion - -using System.Xml.Linq; - -namespace RestSharp.Extensions -{ - /// - /// XML Extension Methods - /// - public static class XmlExtensions - { - /// - /// Returns the name of an element with the namespace if specified - /// - /// Element name - /// XML Namespace - /// - public static XName AsNamespaced(this string name, string @namespace) { - XName xName = name; - - if (@namespace.HasValue()) - xName = XName.Get(name, @namespace); - - return xName; - } - } -} diff --git a/RestSharp/FileParameter.cs b/RestSharp/FileParameter.cs deleted file mode 100644 index 6f1f197e1..000000000 --- a/RestSharp/FileParameter.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System; -using System.IO; - -namespace RestSharp -{ - /// - /// Container for files to be uploaded with requests - /// - public class FileParameter - { - /// - /// Creates a file parameter from an array of bytes. - /// - ///The parameter name to use in the request. - ///The data to use as the file's contents. - ///The filename to use in the request. - ///The content type to use in the request. - ///The - public static FileParameter Create(string name, byte[] data, string filename, string contentType) - { -#if FRAMEWORK && !PocketPC - var length = data.LongLength; -#else - var length = (long)data.Length; -#endif - return new FileParameter - { - Writer = s => s.Write(data, 0, data.Length), - FileName = filename, - ContentType = contentType, - ContentLength = length, - Name = name - }; - } - - /// - /// Creates a file parameter from an array of bytes. - /// - ///The parameter name to use in the request. - ///The data to use as the file's contents. - ///The filename to use in the request. - ///The using the default content type. - public static FileParameter Create(string name, byte[] data, string filename) - { - return Create(name, data, filename, null); - } - - /// - /// The length of data to be sent - /// - public long ContentLength { get; set; } - /// - /// Provides raw data for file - /// - public Action Writer { get; set; } - /// - /// Name of the file to use when uploading - /// - public string FileName { get; set; } - /// - /// MIME content type of file - /// - public string ContentType { get; set; } - /// - /// Name of the parameter - /// - public string Name { get; set; } - } -} diff --git a/RestSharp/Http.Async.cs b/RestSharp/Http.Async.cs deleted file mode 100644 index 50999f349..000000000 --- a/RestSharp/Http.Async.cs +++ /dev/null @@ -1,453 +0,0 @@ -#region License -// Copyright 2010 John Sheehan -// -// 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. -#endregion - -using System; -using System.Net; -using System.Threading; -using RestSharp.Extensions; - -#if SILVERLIGHT -using System.Windows.Browser; -using System.Net.Browser; -#endif - -#if WINDOWS_PHONE -using System.Windows.Threading; -using System.Windows; -#endif - -#if (FRAMEWORK && !MONOTOUCH && !MONODROID && !PocketPC) -using System.Web; -#endif - -namespace RestSharp -{ - /// - /// HttpWebRequest wrapper (async methods) - /// - public partial class Http - { - private TimeOutState _timeoutState; - - public HttpWebRequest DeleteAsync(Action action) - { - return GetStyleMethodInternalAsync("DELETE", action); - } - - public HttpWebRequest GetAsync(Action action) - { - return GetStyleMethodInternalAsync("GET", action); - } - - public HttpWebRequest HeadAsync(Action action) - { - return GetStyleMethodInternalAsync("HEAD", action); - } - - public HttpWebRequest OptionsAsync(Action action) - { - return GetStyleMethodInternalAsync("OPTIONS", action); - } - - public HttpWebRequest PostAsync(Action action) - { - return PutPostInternalAsync("POST", action); - } - - public HttpWebRequest PutAsync(Action action) - { - return PutPostInternalAsync("PUT", action); - } - - public HttpWebRequest PatchAsync(Action action) - { - return PutPostInternalAsync("PATCH", action); - } - - /// - /// Execute an async POST-style request with the specified HTTP Method. - /// - /// The HTTP method to execute. - /// - public HttpWebRequest AsPostAsync(Action action, string httpMethod) - { -#if PocketPC - return PutPostInternalAsync(httpMethod.ToUpper(), action); -#else - return PutPostInternalAsync(httpMethod.ToUpperInvariant(), action); -#endif - } - - /// - /// Execute an async GET-style request with the specified HTTP Method. - /// - /// The HTTP method to execute. - /// - public HttpWebRequest AsGetAsync(Action action, string httpMethod) - { -#if PocketPC - return GetStyleMethodInternalAsync(httpMethod.ToUpper(), action); -#else - return GetStyleMethodInternalAsync(httpMethod.ToUpperInvariant(), action); -#endif - } - - private HttpWebRequest GetStyleMethodInternalAsync(string method, Action callback) - { - HttpWebRequest webRequest = null; - try - { - var url = Url; - webRequest = ConfigureAsyncWebRequest(method, url); - if (HasBody && (method == "DELETE" || method == "OPTIONS")) - { - webRequest.ContentType = RequestContentType; - WriteRequestBodyAsync(webRequest, callback); - } - else - { - _timeoutState = new TimeOutState { Request = webRequest }; - var asyncResult = webRequest.BeginGetResponse(result => ResponseCallback(result, callback), webRequest); - SetTimeout(asyncResult, _timeoutState); - } - } - catch(Exception ex) - { - ExecuteCallback(CreateErrorResponse(ex), callback); - } - return webRequest; - } - - private HttpResponse CreateErrorResponse(Exception ex) - { - var response = new HttpResponse(); - var webException = ex as WebException; - if (webException != null && webException.Status == WebExceptionStatus.RequestCanceled) - { - response.ResponseStatus = _timeoutState.TimedOut ? ResponseStatus.TimedOut : ResponseStatus.Aborted; - return response; - } - - response.ErrorMessage = ex.Message; - response.ErrorException = ex; - response.ResponseStatus = ResponseStatus.Error; - return response; - } - - private HttpWebRequest PutPostInternalAsync(string method, Action callback) - { - HttpWebRequest webRequest = null; - try - { - webRequest = ConfigureAsyncWebRequest(method, Url); - PreparePostBody(webRequest); - WriteRequestBodyAsync(webRequest, callback); - } - catch(Exception ex) - { - ExecuteCallback(CreateErrorResponse(ex), callback); - } - - return webRequest; - } - - private void WriteRequestBodyAsync(HttpWebRequest webRequest, Action callback) - { - IAsyncResult asyncResult; - _timeoutState = new TimeOutState { Request = webRequest }; - - if (HasBody || HasFiles || AlwaysMultipartFormData) - { -#if !WINDOWS_PHONE && !PocketPC - webRequest.ContentLength = CalculateContentLength(); -#endif - asyncResult = webRequest.BeginGetRequestStream(result => RequestStreamCallback(result, callback), webRequest); - } - - else - { - asyncResult = webRequest.BeginGetResponse(r => ResponseCallback(r, callback), webRequest); - } - - SetTimeout(asyncResult, _timeoutState); - } - - private long CalculateContentLength() - { - if (RequestBodyBytes != null) - return RequestBodyBytes.Length; - - if (!HasFiles && !AlwaysMultipartFormData) - { - return _defaultEncoding.GetByteCount(RequestBody); - } - - // calculate length for multipart form - long length = 0; - foreach (var file in Files) - { - length += _defaultEncoding.GetByteCount(GetMultipartFileHeader(file)); - length += file.ContentLength; - length += _defaultEncoding.GetByteCount(_lineBreak); - } - - foreach (var param in Parameters) - { - length += _defaultEncoding.GetByteCount(GetMultipartFormData(param)); - } - - length += _defaultEncoding.GetByteCount(GetMultipartFooter()); - return length; - } - - private void RequestStreamCallback(IAsyncResult result, Action callback) - { - var webRequest = (HttpWebRequest)result.AsyncState; - - if (_timeoutState.TimedOut) - { - var response = new HttpResponse {ResponseStatus = ResponseStatus.TimedOut}; - ExecuteCallback(response, callback); - return; - } - - // write body to request stream - try - { - using(var requestStream = webRequest.EndGetRequestStream(result)) - { - if(HasFiles || AlwaysMultipartFormData) - { - WriteMultipartFormData(requestStream); - } - else if (RequestBodyBytes != null) - { - requestStream.Write(RequestBodyBytes, 0, RequestBodyBytes.Length); - } - else - { - WriteStringTo(requestStream, RequestBody); - } - } - } - catch (Exception ex) - { - ExecuteCallback(CreateErrorResponse(ex), callback); - return; - } - - IAsyncResult asyncResult = webRequest.BeginGetResponse(r => ResponseCallback(r, callback), webRequest); - SetTimeout(asyncResult, _timeoutState); - } - - private void SetTimeout(IAsyncResult asyncResult, TimeOutState timeOutState) - { -#if FRAMEWORK && !PocketPC - if (Timeout != 0) - { - ThreadPool.RegisterWaitForSingleObject(asyncResult.AsyncWaitHandle, new WaitOrTimerCallback(TimeoutCallback), timeOutState, Timeout, true); - } -#endif - } - - private static void TimeoutCallback(object state, bool timedOut) - { - if (!timedOut) - return; - - var timeoutState = state as TimeOutState; - - if (timeoutState == null) - { - return; - } - - lock (timeoutState) - { - timeoutState.TimedOut = true; - } - - if (timeoutState.Request != null) - { - timeoutState.Request.Abort(); - } - } - - private static void GetRawResponseAsync(IAsyncResult result, Action callback) - { - var response = new HttpResponse(); - response.ResponseStatus = ResponseStatus.None; - - HttpWebResponse raw = null; - - try - { - var webRequest = (HttpWebRequest)result.AsyncState; - raw = webRequest.EndGetResponse(result) as HttpWebResponse; - } - catch(WebException ex) - { - if(ex.Status == WebExceptionStatus.RequestCanceled) - { - throw ex; - } - - // Check to see if this is an HTTP error or a transport error. - // In cases where an HTTP error occurs ( status code >= 400 ) - // return the underlying HTTP response, otherwise assume a - // transport exception (ex: connection timeout) and - // rethrow the exception - - if (ex.Response is HttpWebResponse) - { - raw = ex.Response as HttpWebResponse; - } - else - { - throw ex; - } - } - - callback(raw); - raw.Close(); - } - - private void ResponseCallback(IAsyncResult result, Action callback) - { - var response = new HttpResponse {ResponseStatus = ResponseStatus.None}; - - try - { - if(_timeoutState.TimedOut) - { - response.ResponseStatus = ResponseStatus.TimedOut; - ExecuteCallback(response, callback); - return; - } - - GetRawResponseAsync(result, webResponse => - { - ExtractResponseData(response, webResponse); - ExecuteCallback(response, callback); - }); - } - catch(Exception ex) - { - ExecuteCallback(CreateErrorResponse(ex), callback); - } - } - - private static void ExecuteCallback(HttpResponse response, Action callback) - { - callback(response); - } - - partial void AddAsyncHeaderActions() - { -#if SILVERLIGHT - _restrictedHeaderActions.Add("Content-Length", (r, v) => r.ContentLength = Convert.ToInt64(v)); -#endif -#if WINDOWS_PHONE - // WP7 doesn't as of Beta doesn't support a way to set Content-Length either directly - // or indirectly - _restrictedHeaderActions.Add("Content-Length", (r, v) => { }); -#endif - } - - // TODO: Try to merge the shared parts between ConfigureWebRequest and ConfigureAsyncWebRequest (quite a bit of code - // TODO: duplication at the moment). - private HttpWebRequest ConfigureAsyncWebRequest(string method, Uri url) - { -#if SILVERLIGHT - WebRequest.RegisterPrefix("http://", WebRequestCreator.ClientHttp); - WebRequest.RegisterPrefix("https://", WebRequestCreator.ClientHttp); -#endif - var webRequest = (HttpWebRequest)WebRequest.Create(url); -#if !PocketPC - webRequest.UseDefaultCredentials = UseDefaultCredentials; -#endif - webRequest.PreAuthenticate = PreAuthenticate; - - AppendHeaders(webRequest); - AppendCookies(webRequest); - - webRequest.Method = method; - - // make sure Content-Length header is always sent since default is -1 -#if !WINDOWS_PHONE && !PocketPC - // WP7 doesn't as of Beta doesn't support a way to set this value either directly - // or indirectly - if(!HasFiles && !AlwaysMultipartFormData) - { - webRequest.ContentLength = 0; - } -#endif - - if(Credentials != null) - { - webRequest.Credentials = Credentials; - } - -#if !SILVERLIGHT - if(UserAgent.HasValue()) - { - webRequest.UserAgent = UserAgent; - } -#endif - -#if FRAMEWORK - if(ClientCertificates != null) - { - webRequest.ClientCertificates.AddRange(ClientCertificates); - } - - webRequest.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip | DecompressionMethods.None; - ServicePointManager.Expect100Continue = false; - - if (Timeout != 0) - { - webRequest.Timeout = Timeout; - } - - if (ReadWriteTimeout != 0) - { - webRequest.ReadWriteTimeout = ReadWriteTimeout; - } - - if (Proxy != null) - { - webRequest.Proxy = Proxy; - } - - if (FollowRedirects && MaxRedirects.HasValue) - { - webRequest.MaximumAutomaticRedirections = MaxRedirects.Value; - } -#endif - -#if !SILVERLIGHT - webRequest.AllowAutoRedirect = FollowRedirects; -#endif - return webRequest; - } - - private class TimeOutState - { - public bool TimedOut { get; set; } - public HttpWebRequest Request { get; set; } - } - } -} diff --git a/RestSharp/Http.Sync.cs b/RestSharp/Http.Sync.cs deleted file mode 100644 index d3bcfb1ef..000000000 --- a/RestSharp/Http.Sync.cs +++ /dev/null @@ -1,301 +0,0 @@ -#region License -// Copyright 2010 John Sheehan -// -// 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. -#endregion - -#if FRAMEWORK -using System; -using System.Net; - -#if !MONOTOUCH && !MONODROID && !PocketPC -using System.Web; -#endif - -using RestSharp.Extensions; - -namespace RestSharp -{ - /// - /// HttpWebRequest wrapper (sync methods) - /// - public partial class Http - { - /// - /// Execute a POST request - /// - public HttpResponse Post() - { - return PostPutInternal("POST"); - } - - /// - /// Execute a PUT request - /// - public HttpResponse Put() - { - return PostPutInternal("PUT"); - } - - /// - /// Execute a GET request - /// - public HttpResponse Get() - { - return GetStyleMethodInternal("GET"); - } - - /// - /// Execute a HEAD request - /// - public HttpResponse Head() - { - return GetStyleMethodInternal("HEAD"); - } - - /// - /// Execute an OPTIONS request - /// - public HttpResponse Options() - { - return GetStyleMethodInternal("OPTIONS"); - } - - /// - /// Execute a DELETE request - /// - public HttpResponse Delete() - { - return GetStyleMethodInternal("DELETE"); - } - - /// - /// Execute a PATCH request - /// - public HttpResponse Patch() - { - return PostPutInternal("PATCH"); - } - - /// - /// Execute a GET-style request with the specified HTTP Method. - /// - /// The HTTP method to execute. - /// - public HttpResponse AsGet(string httpMethod) - { -#if PocketPC - return GetStyleMethodInternal(httpMethod.ToUpper()); -#else - return GetStyleMethodInternal(httpMethod.ToUpperInvariant()); -#endif - } - - /// - /// Execute a POST-style request with the specified HTTP Method. - /// - /// The HTTP method to execute. - /// - public HttpResponse AsPost(string httpMethod) - { -#if PocketPC - return PostPutInternal(httpMethod.ToUpper()); -#else - return PostPutInternal(httpMethod.ToUpperInvariant()); -#endif - } - - private HttpResponse GetStyleMethodInternal(string method) - { - var webRequest = ConfigureWebRequest(method, Url); - - if (HasBody && (method == "DELETE" || method == "OPTIONS")) - { - webRequest.ContentType = RequestContentType; - WriteRequestBody(webRequest); - } - - return GetResponse(webRequest); - } - - private HttpResponse PostPutInternal(string method) - { - var webRequest = ConfigureWebRequest(method, Url); - - PreparePostData(webRequest); - - WriteRequestBody(webRequest); - return GetResponse(webRequest); - } - - partial void AddSyncHeaderActions() - { - _restrictedHeaderActions.Add("Connection", (r, v) => r.Connection = v); - _restrictedHeaderActions.Add("Content-Length", (r, v) => r.ContentLength = Convert.ToInt64(v)); - _restrictedHeaderActions.Add("Expect", (r, v) => r.Expect = v); - _restrictedHeaderActions.Add("If-Modified-Since", (r, v) => r.IfModifiedSince = Convert.ToDateTime(v)); - _restrictedHeaderActions.Add("Referer", (r, v) => r.Referer = v); - _restrictedHeaderActions.Add("Transfer-Encoding", (r, v) => { r.TransferEncoding = v; r.SendChunked = true; }); - _restrictedHeaderActions.Add("User-Agent", (r, v) => r.UserAgent = v); - } - - private void ExtractErrorResponse(HttpResponse httpResponse, Exception ex) - { - var webException = ex as WebException; - - if (webException != null && webException.Status == WebExceptionStatus.Timeout) - { - httpResponse.ResponseStatus = ResponseStatus.TimedOut; - httpResponse.ErrorMessage = ex.Message; - httpResponse.ErrorException = webException; - return; - } - - httpResponse.ErrorMessage = ex.Message; - httpResponse.ErrorException = ex; - httpResponse.ResponseStatus = ResponseStatus.Error; - } - - private HttpResponse GetResponse(HttpWebRequest request) - { - var response = new HttpResponse { ResponseStatus = ResponseStatus.None }; - - try - { - var webResponse = GetRawResponse(request); - ExtractResponseData(response, webResponse); - } - catch (Exception ex) - { - ExtractErrorResponse(response, ex); - } - - return response; - } - - private static HttpWebResponse GetRawResponse(HttpWebRequest request) - { - try - { - return (HttpWebResponse)request.GetResponse(); - } - catch (WebException ex) - { - // Check to see if this is an HTTP error or a transport error. - // In cases where an HTTP error occurs ( status code >= 400 ) - // return the underlying HTTP response, otherwise assume a - // transport exception (ex: connection timeout) and - // rethrow the exception - - if (ex.Response is HttpWebResponse) - { - return ex.Response as HttpWebResponse; - } - throw; - } - } - - private void PreparePostData(HttpWebRequest webRequest) - { - if (HasFiles || AlwaysMultipartFormData) - { - webRequest.ContentType = GetMultipartFormContentType(); - using (var requestStream = webRequest.GetRequestStream()) - { - WriteMultipartFormData(requestStream); - } - } - - PreparePostBody(webRequest); - } - - private void WriteRequestBody(HttpWebRequest webRequest) - { - if (!HasBody) - return; - - var bytes = RequestBodyBytes == null ? _defaultEncoding.GetBytes(RequestBody) : RequestBodyBytes; - - webRequest.ContentLength = bytes.Length; - - using (var requestStream = webRequest.GetRequestStream()) - { - requestStream.Write(bytes, 0, bytes.Length); - } - } - - // TODO: Try to merge the shared parts between ConfigureWebRequest and ConfigureAsyncWebRequest (quite a bit of code - // TODO: duplication at the moment). - private HttpWebRequest ConfigureWebRequest(string method, Uri url) - { - var webRequest = (HttpWebRequest)WebRequest.Create(url); -#if !PocketPC - webRequest.UseDefaultCredentials = UseDefaultCredentials; -#endif - webRequest.PreAuthenticate = PreAuthenticate; - ServicePointManager.Expect100Continue = false; - - AppendHeaders(webRequest); - AppendCookies(webRequest); - - webRequest.Method = method; - - // make sure Content-Length header is always sent since default is -1 - if (!HasFiles && !AlwaysMultipartFormData) - { - webRequest.ContentLength = 0; - } - - webRequest.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip | DecompressionMethods.None; - - if(ClientCertificates != null) - { - webRequest.ClientCertificates.AddRange(ClientCertificates); - } - - if(UserAgent.HasValue()) - { - webRequest.UserAgent = UserAgent; - } - - if(Timeout != 0) - { - webRequest.Timeout = Timeout; - } - - if (ReadWriteTimeout != 0) - { - webRequest.ReadWriteTimeout = ReadWriteTimeout; - } - - if(Credentials != null) - { - webRequest.Credentials = Credentials; - } - - if(Proxy != null) - { - webRequest.Proxy = Proxy; - } - - webRequest.AllowAutoRedirect = FollowRedirects; - if(FollowRedirects && MaxRedirects.HasValue) - { - webRequest.MaximumAutomaticRedirections = MaxRedirects.Value; - } - - return webRequest; - } - } -} -#endif \ No newline at end of file diff --git a/RestSharp/Http.cs b/RestSharp/Http.cs deleted file mode 100644 index 895a76c2c..000000000 --- a/RestSharp/Http.cs +++ /dev/null @@ -1,451 +0,0 @@ -#region License -// Copyright 2010 John Sheehan -// -// 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. -#endregion - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Security.Cryptography.X509Certificates; -using System.Text; -using RestSharp.Extensions; - -#if WINDOWS_PHONE -using RestSharp.Compression.ZLib; -#endif - -namespace RestSharp -{ - /// - /// HttpWebRequest wrapper - /// - public partial class Http : IHttp, IHttpFactory - { - private const string _lineBreak = "\r\n"; - private static readonly Encoding _defaultEncoding = Encoding.UTF8; - - /// - /// Creates an IHttp - /// - /// - public IHttp Create() - { - return new Http(); - } - - /// - /// True if this HTTP request has any HTTP parameters - /// - protected bool HasParameters - { - get - { - return Parameters.Any(); - } - } - - /// - /// True if this HTTP request has any HTTP cookies - /// - protected bool HasCookies - { - get - { - return Cookies.Any(); - } - } - - /// - /// True if a request body has been specified - /// - protected bool HasBody - { - get - { - return RequestBodyBytes != null || !string.IsNullOrEmpty(RequestBody); - } - } - - /// - /// True if files have been set to be uploaded - /// - protected bool HasFiles - { - get - { - return Files.Any(); - } - } - - /// - /// Always send a multipart/form-data request - even when no Files are present. - /// - public bool AlwaysMultipartFormData { get; set; } - - /// - /// UserAgent to be sent with request - /// - public string UserAgent { get; set; } - /// - /// Timeout in milliseconds to be used for the request - /// - public int Timeout { get; set; } - /// - /// The number of milliseconds before the writing or reading times out. - /// - public int ReadWriteTimeout { get; set; } - /// - /// System.Net.ICredentials to be sent with request - /// - public ICredentials Credentials { get; set; } - /// - /// The System.Net.CookieContainer to be used for the request - /// -#if !PocketPC - public CookieContainer CookieContainer { get; set; } -#endif - /// - /// The method to use to write the response instead of reading into RawBytes - /// - public Action ResponseWriter { get; set; } - /// - /// Collection of files to be sent with request - /// - public IList Files { get; private set; } -#if !SILVERLIGHT - /// - /// Whether or not HTTP 3xx response redirects should be automatically followed - /// - public bool FollowRedirects { get; set; } -#endif -#if FRAMEWORK - /// - /// X509CertificateCollection to be sent with request - /// - public X509CertificateCollection ClientCertificates { get; set; } - /// - /// Maximum number of automatic redirects to follow if FollowRedirects is true - /// - public int? MaxRedirects { get; set; } -#endif - /// - /// Determine whether or not the "default credentials" (e.g. the user account under which the current process is running) - /// will be sent along to the server. - /// -#if !PocketPC - public bool UseDefaultCredentials { get; set; } -#endif - /// - /// HTTP headers to be sent with request - /// - public IList Headers { get; private set; } - /// - /// HTTP parameters (QueryString or Form values) to be sent with request - /// - public IList Parameters { get; private set; } - /// - /// HTTP cookies to be sent with request - /// - public IList Cookies { get; private set; } - /// - /// Request body to be sent with request - /// - public string RequestBody { get; set; } - /// - /// Content type of the request body. - /// - public string RequestContentType { get; set; } - /// - /// An alternative to RequestBody, for when the caller already has the byte array. - /// - public byte[] RequestBodyBytes { get; set; } - /// - /// URL to call for this request - /// - public Uri Url { get; set; } - - public bool PreAuthenticate { get; set; } - -#if FRAMEWORK - /// - /// Proxy info to be sent with request - /// - public IWebProxy Proxy { get; set; } -#endif - - /// - /// Default constructor - /// - public Http() - { - Headers = new List(); - Files = new List(); - Parameters = new List(); - Cookies = new List(); - - _restrictedHeaderActions = new Dictionary>(StringComparer.OrdinalIgnoreCase); - - AddSharedHeaderActions(); - AddSyncHeaderActions(); - } - - partial void AddSyncHeaderActions(); - partial void AddAsyncHeaderActions(); - private void AddSharedHeaderActions() - { - _restrictedHeaderActions.Add("Accept", (r, v) => r.Accept = v); - _restrictedHeaderActions.Add("Content-Type", (r, v) => r.ContentType = v); -#if NET4 - _restrictedHeaderActions.Add("Date", (r, v) => - { - DateTime parsed; - if (DateTime.TryParse(v, out parsed)) - { - r.Date = parsed; - } - }); - _restrictedHeaderActions.Add("Host", (r, v) => r.Host = v); -#else - _restrictedHeaderActions.Add("Date", (r, v) => { /* Set by system */ }); - _restrictedHeaderActions.Add("Host", (r, v) => { /* Set by system */ }); -#endif -#if FRAMEWORK - _restrictedHeaderActions.Add("Range", (r, v) => { AddRange(r, v); }); -#endif - } - - private const string FormBoundary = "-----------------------------28947758029299"; - private static string GetMultipartFormContentType() - { - return string.Format("multipart/form-data; boundary={0}", FormBoundary); - } - - private static string GetMultipartFileHeader (HttpFile file) - { - return string.Format ("--{0}{4}Content-Disposition: form-data; name=\"{1}\"; filename=\"{2}\"{4}Content-Type: {3}{4}{4}", - FormBoundary, file.Name, file.FileName, file.ContentType ?? "application/octet-stream", _lineBreak); - } - - private static string GetMultipartFormData (HttpParameter param) - { - return string.Format ("--{0}{3}Content-Disposition: form-data; name=\"{1}\"{3}{3}{2}{3}", - FormBoundary, param.Name, param.Value, _lineBreak); - } - - private static string GetMultipartFooter () - { - return string.Format ("--{0}--{1}", FormBoundary, _lineBreak); - } - - private readonly IDictionary> _restrictedHeaderActions; - - // handle restricted headers the .NET way - thanks @dimebrain! - // http://msdn.microsoft.com/en-us/library/system.net.httpwebrequest.headers.aspx - private void AppendHeaders(HttpWebRequest webRequest) - { - foreach (var header in Headers) - { - if (_restrictedHeaderActions.ContainsKey(header.Name)) - { - _restrictedHeaderActions[header.Name].Invoke(webRequest, header.Value); - } - else - { -#if FRAMEWORK - webRequest.Headers.Add(header.Name, header.Value); -#else - webRequest.Headers[header.Name] = header.Value; -#endif - } - } - } - - private void AppendCookies(HttpWebRequest webRequest) - { -#if !PocketPC - webRequest.CookieContainer = this.CookieContainer ?? new CookieContainer(); -#endif - foreach (var httpCookie in Cookies) - { -#if !PocketPC -#if FRAMEWORK - var cookie = new Cookie - { - Name = httpCookie.Name, - Value = httpCookie.Value, - Domain = webRequest.RequestUri.Host - }; - webRequest.CookieContainer.Add(cookie); -#else - var cookie = new Cookie - { - Name = httpCookie.Name, - Value = httpCookie.Value - }; - var uri = webRequest.RequestUri; - webRequest.CookieContainer.Add(new Uri(string.Format("{0}://{1}", uri.Scheme, uri.Host)), cookie); -#endif -#endif - } - } - - private string EncodeParameters() - { - var querystring = new StringBuilder(); - foreach (var p in Parameters) - { - if (querystring.Length > 1) - querystring.Append("&"); - querystring.AppendFormat("{0}={1}", p.Name.UrlEncode(), p.Value.UrlEncode()); - } - - return querystring.ToString(); - } - - private void PreparePostBody(HttpWebRequest webRequest) - { - if (HasFiles || AlwaysMultipartFormData) - { - webRequest.ContentType = GetMultipartFormContentType(); - } - else if(HasParameters) - { - webRequest.ContentType = "application/x-www-form-urlencoded"; - RequestBody = EncodeParameters(); - } - else if(HasBody) - { - webRequest.ContentType = RequestContentType; - } - } - - private static void WriteStringTo(Stream stream, string toWrite) - { - var bytes = _defaultEncoding.GetBytes(toWrite); - stream.Write(bytes, 0, bytes.Length); - } - - private void WriteMultipartFormData(Stream requestStream) - { - foreach (var param in Parameters) - { - WriteStringTo(requestStream, GetMultipartFormData(param)); - } - - foreach (var file in Files) - { - // Add just the first part of this param, since we will write the file data directly to the Stream - WriteStringTo(requestStream, GetMultipartFileHeader(file)); - - // Write the file data directly to the Stream, rather than serializing it to a string. - file.Writer(requestStream); - WriteStringTo(requestStream, _lineBreak); - } - - WriteStringTo(requestStream, GetMultipartFooter()); - } - - private void ExtractResponseData(HttpResponse response, HttpWebResponse webResponse) - { - using (webResponse) - { -#if FRAMEWORK - response.ContentEncoding = webResponse.ContentEncoding; - response.Server = webResponse.Server; -#endif - response.ContentType = webResponse.ContentType; - response.ContentLength = webResponse.ContentLength; - Stream webResponseStream = webResponse.GetResponseStream(); -#if WINDOWS_PHONE - if (String.Equals(webResponse.Headers[HttpRequestHeader.ContentEncoding], "gzip", StringComparison.OrdinalIgnoreCase)) - { - var gzStream = new GZipStream(webResponseStream); - ProcessResponseStream(gzStream, response); - } - else - { - ProcessResponseStream(webResponseStream, response); - } -#else - ProcessResponseStream(webResponseStream, response); -#endif - response.StatusCode = webResponse.StatusCode; - response.StatusDescription = webResponse.StatusDescription; - response.ResponseUri = webResponse.ResponseUri; - response.ResponseStatus = ResponseStatus.Completed; - -#if !PocketPC - if (webResponse.Cookies != null) - { - foreach (Cookie cookie in webResponse.Cookies) - { - response.Cookies.Add(new HttpCookie { - Comment = cookie.Comment, - CommentUri = cookie.CommentUri, - Discard = cookie.Discard, - Domain = cookie.Domain, - Expired = cookie.Expired, - Expires = cookie.Expires, - HttpOnly = cookie.HttpOnly, - Name = cookie.Name, - Path = cookie.Path, - Port = cookie.Port, - Secure = cookie.Secure, - TimeStamp = cookie.TimeStamp, - Value = cookie.Value, - Version = cookie.Version - }); - } - } -#endif - foreach (var headerName in webResponse.Headers.AllKeys) - { - var headerValue = webResponse.Headers[headerName]; - response.Headers.Add(new HttpHeader { Name = headerName, Value = headerValue }); - } - - webResponse.Close(); - } - } - - private void ProcessResponseStream(Stream webResponseStream, HttpResponse response) - { - if (ResponseWriter == null) - { - response.RawBytes = webResponseStream.ReadAsBytes(); - } - else - { - ResponseWriter(webResponseStream); - } - } - -#if FRAMEWORK - private void AddRange(HttpWebRequest r, string range) - { - System.Text.RegularExpressions.Match m = System.Text.RegularExpressions.Regex.Match(range, "=(\\d+)-(\\d+)$"); - if (!m.Success) - { - return; - } - - int from = Convert.ToInt32(m.Groups[1].Value); - int to = Convert.ToInt32(m.Groups[2].Value); - r.AddRange(from, to); - } -#endif - } -} \ No newline at end of file diff --git a/RestSharp/HttpCookie.cs b/RestSharp/HttpCookie.cs deleted file mode 100644 index 7382590d5..000000000 --- a/RestSharp/HttpCookie.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; - -namespace RestSharp -{ - /// - /// Representation of an HTTP cookie - /// - public class HttpCookie - { - /// - /// Comment of the cookie - /// - public string Comment { get; set; } - /// - /// Comment of the cookie - /// - public Uri CommentUri { get; set; } - /// - /// Indicates whether the cookie should be discarded at the end of the session - /// - public bool Discard { get; set; } - /// - /// Domain of the cookie - /// - public string Domain { get; set; } - /// - /// Indicates whether the cookie is expired - /// - public bool Expired { get; set; } - /// - /// Date and time that the cookie expires - /// - public DateTime Expires { get; set; } - /// - /// Indicates that this cookie should only be accessed by the server - /// - public bool HttpOnly { get; set; } - /// - /// Name of the cookie - /// - public string Name { get; set; } - /// - /// Path of the cookie - /// - public string Path { get; set; } - /// - /// Port of the cookie - /// - public string Port { get; set; } - /// - /// Indicates that the cookie should only be sent over secure channels - /// - public bool Secure { get; set; } - /// - /// Date and time the cookie was created - /// - public DateTime TimeStamp { get; set; } - /// - /// Value of the cookie - /// - public string Value { get; set; } - /// - /// Version of the cookie - /// - public int Version { get; set; } - } -} diff --git a/RestSharp/HttpFile.cs b/RestSharp/HttpFile.cs deleted file mode 100644 index bd7ddbdf7..000000000 --- a/RestSharp/HttpFile.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.IO; - -namespace RestSharp -{ - /// - /// Container for HTTP file - /// - public class HttpFile - { - - /// - /// The length of data to be sent - /// - public long ContentLength { get; set; } - /// - /// Provides raw data for file - /// - public Action Writer { get; set; } - /// - /// Name of the file to use when uploading - /// - public string FileName { get; set; } - /// - /// MIME content type of file - /// - public string ContentType { get; set; } - /// - /// Name of the parameter - /// - public string Name { get; set; } - } -} diff --git a/RestSharp/HttpHeader.cs b/RestSharp/HttpHeader.cs deleted file mode 100644 index f2ef461f0..000000000 --- a/RestSharp/HttpHeader.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace RestSharp -{ - /// - /// Representation of an HTTP header - /// - public class HttpHeader - { - /// - /// Name of the header - /// - public string Name { get; set; } - /// - /// Value of the header - /// - public string Value { get; set; } - } -} diff --git a/RestSharp/HttpParameter.cs b/RestSharp/HttpParameter.cs deleted file mode 100644 index 8423a80fb..000000000 --- a/RestSharp/HttpParameter.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace RestSharp -{ - /// - /// Representation of an HTTP parameter (QueryString or Form value) - /// - public class HttpParameter - { - /// - /// Name of the parameter - /// - public string Name { get; set; } - /// - /// Value of the parameter - /// - public string Value { get; set; } - } -} diff --git a/RestSharp/HttpResponse.cs b/RestSharp/HttpResponse.cs deleted file mode 100644 index 83ffb782e..000000000 --- a/RestSharp/HttpResponse.cs +++ /dev/null @@ -1,122 +0,0 @@ -#region License -// Copyright 2010 John Sheehan -// -// 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. -#endregion - -using System; -using System.Collections.Generic; -using System.Net; -using RestSharp.Extensions; - -namespace RestSharp -{ - /// - /// HTTP response data - /// - public class HttpResponse : IHttpResponse - { - private string _content; - - /// - /// Default constructor - /// - public HttpResponse() - { - Headers = new List(); - Cookies = new List(); - } - - /// - /// MIME content type of response - /// - public string ContentType { get; set; } - /// - /// Length in bytes of the response content - /// - public long ContentLength { get; set; } - /// - /// Encoding of the response content - /// - public string ContentEncoding { get; set; } - /// - /// Lazy-loaded string representation of response content - /// - public string Content - { - get - { - if (_content == null) - { - _content = RawBytes.AsString(); - } - return _content; - } - } - /// - /// HTTP response status code - /// - public HttpStatusCode StatusCode { get; set; } - /// - /// Description of HTTP status returned - /// - public string StatusDescription { get; set; } - /// - /// Response content - /// - public byte[] RawBytes { get; set; } - /// - /// The URL that actually responded to the content (different from request if redirected) - /// - public Uri ResponseUri { get; set; } - /// - /// HttpWebResponse.Server - /// - public string Server { get; set; } - /// - /// Headers returned by server with the response - /// - public IList Headers { get; private set; } - /// - /// Cookies returned by server with the response - /// - public IList Cookies { get; private set; } - - private ResponseStatus _responseStatus = ResponseStatus.None; - /// - /// Status of the request. Will return Error for transport errors. - /// HTTP errors will still return ResponseStatus.Completed, check StatusCode instead - /// - public ResponseStatus ResponseStatus - { - get - { - return _responseStatus; - } - set - { - _responseStatus = value; - } - } - - /// - /// Transport or other non-HTTP error generated while attempting request - /// - public string ErrorMessage { get; set; } - - /// - /// Exception thrown when error is encountered. - /// - public Exception ErrorException { get; set; } - } -} diff --git a/RestSharp/IHttp.cs b/RestSharp/IHttp.cs deleted file mode 100644 index 782b59432..000000000 --- a/RestSharp/IHttp.cs +++ /dev/null @@ -1,90 +0,0 @@ -#region License -// Copyright 2010 John Sheehan -// -// 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. -#endregion - -using System; -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Security.Cryptography.X509Certificates; - -namespace RestSharp -{ - public interface IHttp - { - Action ResponseWriter { get; set; } -#if !PocketPC - CookieContainer CookieContainer { get; set; } -#endif - ICredentials Credentials { get; set; } - - /// - /// Always send a multipart/form-data request - even when no Files are present. - /// - bool AlwaysMultipartFormData { get; set; } - - string UserAgent { get; set; } - int Timeout { get; set; } - int ReadWriteTimeout { get; set; } -#if !SILVERLIGHT - bool FollowRedirects { get; set; } -#endif -#if FRAMEWORK - X509CertificateCollection ClientCertificates { get; set; } - int? MaxRedirects { get; set; } -#endif -#if !PocketPC - bool UseDefaultCredentials { get; set; } -#endif - IList Headers { get; } - IList Parameters { get; } - IList Files { get; } - IList Cookies { get; } - string RequestBody { get; set; } - string RequestContentType { get; set; } - bool PreAuthenticate { get; set; } - - /// - /// An alternative to RequestBody, for when the caller already has the byte array. - /// - byte[] RequestBodyBytes { get; set; } - - Uri Url { get; set; } - - HttpWebRequest DeleteAsync(Action action); - HttpWebRequest GetAsync(Action action); - HttpWebRequest HeadAsync(Action action); - HttpWebRequest OptionsAsync(Action action); - HttpWebRequest PostAsync(Action action); - HttpWebRequest PutAsync(Action action); - HttpWebRequest PatchAsync(Action action); - HttpWebRequest AsPostAsync(Action action, string httpMethod); - HttpWebRequest AsGetAsync(Action action, string httpMethod); - -#if FRAMEWORK - HttpResponse Delete(); - HttpResponse Get(); - HttpResponse Head(); - HttpResponse Options(); - HttpResponse Post(); - HttpResponse Put(); - HttpResponse Patch(); - HttpResponse AsPost(string httpMethod); - HttpResponse AsGet(string httpMethod); - - IWebProxy Proxy { get; set; } -#endif - } -} \ No newline at end of file diff --git a/RestSharp/IHttpFactory.cs b/RestSharp/IHttpFactory.cs deleted file mode 100644 index bbcad8463..000000000 --- a/RestSharp/IHttpFactory.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace RestSharp -{ - public interface IHttpFactory - { - IHttp Create(); - } - - public class SimpleFactory : IHttpFactory where T : IHttp, new() - { - public IHttp Create() - { - return new T(); - } - } -} diff --git a/RestSharp/IHttpResponse.cs b/RestSharp/IHttpResponse.cs deleted file mode 100644 index 91f05dec1..000000000 --- a/RestSharp/IHttpResponse.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net; - -namespace RestSharp -{ - /// - /// HTTP response data - /// - public interface IHttpResponse - { - /// - /// MIME content type of response - /// - string ContentType { get; set; } - - /// - /// Length in bytes of the response content - /// - long ContentLength { get; set; } - - /// - /// Encoding of the response content - /// - string ContentEncoding { get; set; } - - /// - /// String representation of response content - /// - string Content { get; } - - /// - /// HTTP response status code - /// - HttpStatusCode StatusCode { get; set; } - - /// - /// Description of HTTP status returned - /// - string StatusDescription { get; set; } - - /// - /// Response content - /// - byte[] RawBytes { get; set; } - - /// - /// The URL that actually responded to the content (different from request if redirected) - /// - Uri ResponseUri { get; set; } - - /// - /// HttpWebResponse.Server - /// - string Server { get; set; } - - /// - /// Headers returned by server with the response - /// - IList Headers { get; } - - /// - /// Cookies returned by server with the response - /// - IList Cookies { get; } - - /// - /// Status of the request. Will return Error for transport errors. - /// HTTP errors will still return ResponseStatus.Completed, check StatusCode instead - /// - ResponseStatus ResponseStatus { get; set; } - - /// - /// Transport or other non-HTTP error generated while attempting request - /// - string ErrorMessage { get; set; } - - /// - /// Exception thrown when error is encountered. - /// - Exception ErrorException { get; set; } - } -} \ No newline at end of file diff --git a/RestSharp/IRestClient.cs b/RestSharp/IRestClient.cs deleted file mode 100644 index 0a8ce2c29..000000000 --- a/RestSharp/IRestClient.cs +++ /dev/null @@ -1,222 +0,0 @@ -#region License -// Copyright 2010 John Sheehan -// -// 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. -#endregion - -using System; -using System.Net; -using System.Collections.Generic; -using System.Security.Cryptography.X509Certificates; -#if NET4 || MONODROID || MONOTOUCH || WP8 -using System.Threading; -using System.Threading.Tasks; -#endif - -namespace RestSharp -{ - /// - /// - /// - public interface IRestClient - { - /// - /// - /// -#if !PocketPC - CookieContainer CookieContainer { get; set; } -#endif - /// - /// - /// - string UserAgent { get; set; } - /// - /// - /// - int Timeout { get; set; } - /// - /// - /// - int ReadWriteTimeout { get; set; } - /// - /// - /// - bool UseSynchronizationContext { get; set; } - /// - /// - /// - IAuthenticator Authenticator { get; set; } - /// - /// - /// - string BaseUrl { get; set; } - /// - /// - /// - bool PreAuthenticate { get; set; } - /// - /// - /// - IList DefaultParameters { get; } - /// - /// - /// - /// - RestRequestAsyncHandle ExecuteAsync(IRestRequest request, Action callback); - /// - /// - /// - /// - RestRequestAsyncHandle ExecuteAsync(IRestRequest request, Action, RestRequestAsyncHandle> callback); - -#if FRAMEWORK - /// - /// X509CertificateCollection to be sent with request - /// - X509CertificateCollection ClientCertificates { get; set; } - IRestResponse Execute(IRestRequest request); - IRestResponse Execute(IRestRequest request) where T : new(); - - IWebProxy Proxy { get; set; } -#endif - - Uri BuildUri(IRestRequest request); - - /// - /// Executes a GET-style request and callback asynchronously, authenticating if needed - /// - /// Request to be executed - /// Callback function to be executed upon completion providing access to the async handle. - /// The HTTP method to execute - RestRequestAsyncHandle ExecuteAsyncGet(IRestRequest request, Action callback, string httpMethod); - - /// - /// Executes a POST-style request and callback asynchronously, authenticating if needed - /// - /// Request to be executed - /// Callback function to be executed upon completion providing access to the async handle. - /// The HTTP method to execute - RestRequestAsyncHandle ExecuteAsyncPost(IRestRequest request, Action callback, string httpMethod); - - /// - /// Executes a GET-style request and callback asynchronously, authenticating if needed - /// - /// Target deserialization type - /// Request to be executed - /// Callback function to be executed upon completion - /// The HTTP method to execute - RestRequestAsyncHandle ExecuteAsyncGet(IRestRequest request, Action, RestRequestAsyncHandle> callback, string httpMethod); - - /// - /// Executes a GET-style request and callback asynchronously, authenticating if needed - /// - /// Target deserialization type - /// Request to be executed - /// Callback function to be executed upon completion - /// The HTTP method to execute - RestRequestAsyncHandle ExecuteAsyncPost(IRestRequest request, Action, RestRequestAsyncHandle> callback, string httpMethod); - -#if FRAMEWORK - IRestResponse ExecuteAsGet(IRestRequest request, string httpMethod); - IRestResponse ExecuteAsPost(IRestRequest request, string httpMethod); - IRestResponse ExecuteAsGet(IRestRequest request, string httpMethod) where T : new(); - IRestResponse ExecuteAsPost(IRestRequest request, string httpMethod) where T : new(); -#endif - -#if NET4 || MONODROID || MONOTOUCH || WP8 - /// - /// Executes the request and callback asynchronously, authenticating if needed - /// - /// Target deserialization type - /// Request to be executed - /// The cancellation token - Task> ExecuteTaskAsync(IRestRequest request, CancellationToken token); - - /// - /// Executes the request asynchronously, authenticating if needed - /// - /// Target deserialization type - /// Request to be executed - Task> ExecuteTaskAsync(IRestRequest request); - - /// - /// Executes a GET-style request asynchronously, authenticating if needed - /// - /// Target deserialization type - /// Request to be executed - Task> ExecuteGetTaskAsync(IRestRequest request); - - /// - /// Executes a GET-style request asynchronously, authenticating if needed - /// - /// Target deserialization type - /// Request to be executed - /// The cancellation token - Task> ExecuteGetTaskAsync(IRestRequest request, CancellationToken token); - - /// - /// Executes a POST-style request asynchronously, authenticating if needed - /// - /// Target deserialization type - /// Request to be executed - Task> ExecutePostTaskAsync(IRestRequest request); - - /// - /// Executes a POST-style request asynchronously, authenticating if needed - /// - /// Target deserialization type - /// Request to be executed - /// The cancellation token - Task> ExecutePostTaskAsync(IRestRequest request, CancellationToken token); - - /// - /// Executes the request and callback asynchronously, authenticating if needed - /// - /// Request to be executed - /// The cancellation token - Task ExecuteTaskAsync(IRestRequest request, CancellationToken token); - - /// - /// Executes the request asynchronously, authenticating if needed - /// - /// Request to be executed - Task ExecuteTaskAsync(IRestRequest request); - - /// - /// Executes a GET-style asynchronously, authenticating if needed - /// - /// Request to be executed - Task ExecuteGetTaskAsync(IRestRequest request); - - /// - /// Executes a GET-style asynchronously, authenticating if needed - /// - /// Request to be executed - /// The cancellation token - Task ExecuteGetTaskAsync(IRestRequest request, CancellationToken token); - - /// - /// Executes a POST-style asynchronously, authenticating if needed - /// - /// Request to be executed - Task ExecutePostTaskAsync(IRestRequest request); - - /// - /// Executes a POST-style asynchronously, authenticating if needed - /// - /// Request to be executed - /// The cancellation token - Task ExecutePostTaskAsync(IRestRequest request, CancellationToken token); -#endif - } -} diff --git a/RestSharp/IRestRequest.cs b/RestSharp/IRestRequest.cs deleted file mode 100644 index 942c64cdc..000000000 --- a/RestSharp/IRestRequest.cs +++ /dev/null @@ -1,251 +0,0 @@ -#region License -// Copyright 2010 John Sheehan -// -// 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. -#endregion - -using System; -using System.Collections.Generic; -using System.IO; -using System.Net; -using RestSharp.Serializers; - -namespace RestSharp -{ - public interface IRestRequest - { - /// - /// Always send a multipart/form-data request - even when no Files are present. - /// - bool AlwaysMultipartFormData { get; set; } - - /// - /// Serializer to use when writing JSON request bodies. Used if RequestFormat is Json. - /// By default the included JsonSerializer is used (currently using JSON.NET default serialization). - /// - ISerializer JsonSerializer { get; set; } - - /// - /// Serializer to use when writing XML request bodies. Used if RequestFormat is Xml. - /// By default the included XmlSerializer is used. - /// - ISerializer XmlSerializer { get; set; } - - /// - /// Set this to write response to Stream rather than reading into memory. - /// - Action ResponseWriter { get; set; } - - /// - /// Container of all HTTP parameters to be passed with the request. - /// See AddParameter() for explanation of the types of parameters that can be passed - /// - List Parameters { get; } - - /// - /// Container of all the files to be uploaded with the request. - /// - List Files { get; } - - /// - /// Determines what HTTP method to use for this request. Supported methods: GET, POST, PUT, DELETE, HEAD, OPTIONS - /// Default is GET - /// - Method Method { get; set; } - - /// - /// The Resource URL to make the request against. - /// Tokens are substituted with UrlSegment parameters and match by name. - /// Should not include the scheme or domain. Do not include leading slash. - /// Combined with RestClient.BaseUrl to assemble final URL: - /// {BaseUrl}/{Resource} (BaseUrl is scheme + domain, e.g. http://example.com) - /// - /// - /// // example for url token replacement - /// request.Resource = "Products/{ProductId}"; - /// request.AddParameter("ProductId", 123, ParameterType.UrlSegment); - /// - string Resource { get; set; } - - /// - /// Serializer to use when writing XML request bodies. Used if RequestFormat is Xml. - /// By default XmlSerializer is used. - /// - DataFormat RequestFormat { get; set; } - - /// - /// Used by the default deserializers to determine where to start deserializing from. - /// Can be used to skip container or root elements that do not have corresponding deserialzation targets. - /// - string RootElement { get; set; } - - /// - /// Used by the default deserializers to explicitly set which date format string to use when parsing dates. - /// - string DateFormat { get; set; } - - /// - /// Used by XmlDeserializer. If not specified, XmlDeserializer will flatten response by removing namespaces from element names. - /// - string XmlNamespace { get; set; } - - /// - /// In general you would not need to set this directly. Used by the NtlmAuthenticator. - /// - ICredentials Credentials { get; set; } - - /// - /// Timeout in milliseconds to be used for the request. This timeout value overrides a timeout set on the RestClient. - /// - int Timeout { get; set; } - - /// - /// The number of milliseconds before the writing or reading times out. This timeout value overrides a timeout set on the RestClient. - /// - int ReadWriteTimeout { get; set; } - - /// - /// How many attempts were made to send this Request? - /// - /// - /// This Number is incremented each time the RestClient sends the request. - /// Useful when using Asynchronous Execution with Callbacks - /// - int Attempts { get; } - - /// - /// Determine whether or not the "default credentials" (e.g. the user account under which the current process is running) - /// will be sent along to the server. The default is false. - /// - bool UseDefaultCredentials { get; set; } - -#if FRAMEWORK - /// - /// Adds a file to the Files collection to be included with a POST or PUT request - /// (other methods do not support file uploads). - /// - /// The parameter name to use in the request - /// Full path to file to upload - /// This request - IRestRequest AddFile (string name, string path); - - /// - /// Adds the bytes to the Files collection with the specified file name - /// - /// The parameter name to use in the request - /// The file data - /// The file name to use for the uploaded file - /// This request - IRestRequest AddFile (string name, byte[] bytes, string fileName); - - /// - /// Adds the bytes to the Files collection with the specified file name and content type - /// - /// The parameter name to use in the request - /// The file data - /// The file name to use for the uploaded file - /// The MIME type of the file to upload - /// This request - IRestRequest AddFile (string name, byte[] bytes, string fileName, string contentType); -#endif - - /// - /// Serializes obj to format specified by RequestFormat, but passes xmlNamespace if using the default XmlSerializer - /// - /// The object to serialize - /// The XML namespace to use when serializing - /// This request - IRestRequest AddBody (object obj, string xmlNamespace); - - /// - /// Serializes obj to data format specified by RequestFormat and adds it to the request body. - /// - /// The object to serialize - /// This request - IRestRequest AddBody (object obj); - - /// - /// Calls AddParameter() for all public, readable properties specified in the white list - /// - /// - /// request.AddObject(product, "ProductId", "Price", ...); - /// - /// The object with properties to add as parameters - /// The names of the properties to include - /// This request - IRestRequest AddObject (object obj, params string[] whitelist); - - /// - /// Calls AddParameter() for all public, readable properties of obj - /// - /// The object with properties to add as parameters - /// This request - IRestRequest AddObject (object obj); - - /// - /// Add the parameter to the request - /// - /// Parameter to add - /// - IRestRequest AddParameter (Parameter p); - - /// - /// Adds a HTTP parameter to the request (QueryString for GET, DELETE, OPTIONS and HEAD; Encoded form for POST and PUT) - /// - /// Name of the parameter - /// Value of the parameter - /// This request - IRestRequest AddParameter (string name, object value); - - /// - /// Adds a parameter to the request. There are five types of parameters: - /// - GetOrPost: Either a QueryString value or encoded form value based on method - /// - HttpHeader: Adds the name/value pair to the HTTP request's Headers collection - /// - UrlSegment: Inserted into URL if there is a matching url token e.g. {AccountId} - /// - Cookie: Adds the name/value pair to the HTTP request's Cookies collection - /// - RequestBody: Used by AddBody() (not recommended to use directly) - /// - /// Name of the parameter - /// Value of the parameter - /// The type of parameter to add - /// This request - IRestRequest AddParameter (string name, object value, ParameterType type); - - /// - /// Shortcut to AddParameter(name, value, HttpHeader) overload - /// - /// Name of the header to add - /// Value of the header to add - /// - IRestRequest AddHeader (string name, string value); - - /// - /// Shortcut to AddParameter(name, value, Cookie) overload - /// - /// Name of the cookie to add - /// Value of the cookie to add - /// - IRestRequest AddCookie (string name, string value); - - /// - /// Shortcut to AddParameter(name, value, UrlSegment) overload - /// - /// Name of the segment to add - /// Value of the segment to add - /// - IRestRequest AddUrlSegment(string name, string value); - - Action OnBeforeDeserialization { get; set; } - void IncreaseNumAttempts(); - } -} \ No newline at end of file diff --git a/RestSharp/IRestResponse.cs b/RestSharp/IRestResponse.cs deleted file mode 100644 index 8573401c5..000000000 --- a/RestSharp/IRestResponse.cs +++ /dev/null @@ -1,104 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net; - -namespace RestSharp -{ - /// - /// Container for data sent back from API - /// - public interface IRestResponse - { - /// - /// The RestRequest that was made to get this RestResponse - /// - /// - /// Mainly for debugging if ResponseStatus is not OK - /// - IRestRequest Request { get; set; } - - /// - /// MIME content type of response - /// - string ContentType { get; set; } - - /// - /// Length in bytes of the response content - /// - long ContentLength { get; set; } - - /// - /// Encoding of the response content - /// - string ContentEncoding { get; set; } - - /// - /// String representation of response content - /// - string Content { get; set; } - - /// - /// HTTP response status code - /// - HttpStatusCode StatusCode { get; set; } - - /// - /// Description of HTTP status returned - /// - string StatusDescription { get; set; } - - /// - /// Response content - /// - byte[] RawBytes { get; set; } - - /// - /// The URL that actually responded to the content (different from request if redirected) - /// - Uri ResponseUri { get; set; } - - /// - /// HttpWebResponse.Server - /// - string Server { get; set; } - - /// - /// Cookies returned by server with the response - /// - IList Cookies { get; } - - /// - /// Headers returned by server with the response - /// - IList Headers { get; } - - /// - /// Status of the request. Will return Error for transport errors. - /// HTTP errors will still return ResponseStatus.Completed, check StatusCode instead - /// - ResponseStatus ResponseStatus { get; set; } - - /// - /// Transport or other non-HTTP error generated while attempting request - /// - string ErrorMessage { get; set; } - - /// - /// Exceptions thrown during the request, if any. - /// - /// Will contain only network transport or framework exceptions thrown during the request. HTTP protocol errors are handled by RestSharp and will not appear here. - Exception ErrorException { get; set; } - } - - /// - /// Container for data sent back from API including deserialized data - /// - /// Type of data to deserialize to - public interface IRestResponse : IRestResponse - { - /// - /// Deserialized entity data - /// - T Data { get; set; } - } -} \ No newline at end of file diff --git a/RestSharp/Parameter.cs b/RestSharp/Parameter.cs deleted file mode 100644 index 1b56b4045..000000000 --- a/RestSharp/Parameter.cs +++ /dev/null @@ -1,45 +0,0 @@ -#region License -// Copyright 2010 John Sheehan -// -// 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. -#endregion - -namespace RestSharp -{ - /// - /// Parameter container for REST requests - /// - public class Parameter - { - /// - /// Name of the parameter - /// - public string Name { get; set; } - /// - /// Value of the parameter - /// - public object Value { get; set; } - /// - /// Type of the parameter - /// - public ParameterType Type { get; set; } - - /// - /// Return a human-readable representation of this parameter - /// - /// String - public override string ToString() { - return string.Format("{0}={1}", Name, Value); - } - } -} diff --git a/RestSharp/Properties/AssemblyInfo.cs b/RestSharp/Properties/AssemblyInfo.cs deleted file mode 100644 index c176fb862..000000000 --- a/RestSharp/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("RestSharp")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("d2b12a34-b748-47f9-8ad6-f84da992c64b")] - -[assembly: InternalsVisibleTo("RestSharp.IntegrationTests")] \ No newline at end of file diff --git a/RestSharp/RestClient.Async.cs b/RestSharp/RestClient.Async.cs deleted file mode 100644 index a4d7d68b3..000000000 --- a/RestSharp/RestClient.Async.cs +++ /dev/null @@ -1,386 +0,0 @@ -#region License -// Copyright 2010 John Sheehan -// -// 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. -#endregion - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -#if NET4 || MONODROID || MONOTOUCH || WP8 -using System.Threading.Tasks; -#endif -using System.Text; -using System.Net; - -using RestSharp.Extensions; - -namespace RestSharp -{ - public partial class RestClient - { - /// - /// Executes the request and callback asynchronously, authenticating if needed - /// - /// Request to be executed - /// Callback function to be executed upon completion providing access to the async handle. - public virtual RestRequestAsyncHandle ExecuteAsync(IRestRequest request, Action callback) - { -#if PocketPC - string method = request.Method.ToString(); -#else - string method = Enum.GetName(typeof (Method), request.Method); -#endif - switch (request.Method) - { - case Method.PATCH: - case Method.POST: - case Method.PUT: - return ExecuteAsync(request, callback, method, DoAsPostAsync); - default: - return ExecuteAsync(request, callback, method, DoAsGetAsync); - } - } - - /// - /// Executes a GET-style request and callback asynchronously, authenticating if needed - /// - /// Request to be executed - /// Callback function to be executed upon completion providing access to the async handle. - /// The HTTP method to execute - public virtual RestRequestAsyncHandle ExecuteAsyncGet(IRestRequest request, Action callback, string httpMethod) - { - return ExecuteAsync(request, callback, httpMethod, DoAsGetAsync); - } - - /// - /// Executes a POST-style request and callback asynchronously, authenticating if needed - /// - /// Request to be executed - /// Callback function to be executed upon completion providing access to the async handle. - /// The HTTP method to execute - public virtual RestRequestAsyncHandle ExecuteAsyncPost(IRestRequest request, Action callback, string httpMethod) - { - request.Method = Method.POST; // Required by RestClient.BuildUri... - return ExecuteAsync(request, callback, httpMethod, DoAsPostAsync); - } - - private RestRequestAsyncHandle ExecuteAsync(IRestRequest request, Action callback, string httpMethod, Func, string, HttpWebRequest> getWebRequest) - { - var http = HttpFactory.Create(); - AuthenticateIfNeeded(this, request); - - ConfigureHttp(request, http); - - var asyncHandle = new RestRequestAsyncHandle(); - - Action response_cb = r => ProcessResponse(request, r, asyncHandle, callback); - -#if !PocketPC - if (UseSynchronizationContext && SynchronizationContext.Current != null) - { - var ctx = SynchronizationContext.Current; - var cb = response_cb; - - response_cb = resp => ctx.Post(s => cb(resp), null); - } -#endif - asyncHandle.WebRequest = getWebRequest(http, response_cb, httpMethod); - return asyncHandle; - } - - private static HttpWebRequest DoAsGetAsync(IHttp http, Action response_cb, string method) - { - return http.AsGetAsync(response_cb, method); - } - - private static HttpWebRequest DoAsPostAsync(IHttp http, Action response_cb, string method) - { - return http.AsPostAsync(response_cb, method); - } - - private void ProcessResponse(IRestRequest request, HttpResponse httpResponse, RestRequestAsyncHandle asyncHandle, Action callback) - { - var restResponse = ConvertToRestResponse(request, httpResponse); - callback(restResponse, asyncHandle); - } - - /// - /// Executes the request and callback asynchronously, authenticating if needed - /// - /// Target deserialization type - /// Request to be executed - /// Callback function to be executed upon completion - public virtual RestRequestAsyncHandle ExecuteAsync(IRestRequest request, Action, RestRequestAsyncHandle> callback) - { - return ExecuteAsync(request, (response, asyncHandle) => DeserializeResponse(request, callback, response, asyncHandle)); - } - - /// - /// Executes a GET-style request and callback asynchronously, authenticating if needed - /// - /// Target deserialization type - /// Request to be executed - /// Callback function to be executed upon completion - /// The HTTP method to execute - public virtual RestRequestAsyncHandle ExecuteAsyncGet(IRestRequest request, Action, RestRequestAsyncHandle> callback, string httpMethod) - { - return ExecuteAsyncGet(request, (response, asyncHandle) => DeserializeResponse(request, callback, response, asyncHandle), httpMethod); - } - - /// - /// Executes a POST-style request and callback asynchronously, authenticating if needed - /// - /// Target deserialization type - /// Request to be executed - /// Callback function to be executed upon completion - /// The HTTP method to execute - public virtual RestRequestAsyncHandle ExecuteAsyncPost(IRestRequest request, Action, RestRequestAsyncHandle> callback, string httpMethod) - { - return ExecuteAsyncPost(request, (response, asyncHandle) => DeserializeResponse(request, callback, response, asyncHandle), httpMethod); - } - - private void DeserializeResponse(IRestRequest request, Action, RestRequestAsyncHandle> callback, IRestResponse response, RestRequestAsyncHandle asyncHandle) - { - IRestResponse restResponse = Deserialize(request, response); - callback(restResponse, asyncHandle); - } - -#if NET4 || MONODROID || MONOTOUCH || WP8 - /// - /// Executes a GET-style request asynchronously, authenticating if needed - /// - /// Target deserialization type - /// Request to be executed - public virtual Task> ExecuteGetTaskAsync(IRestRequest request) - { - return ExecuteGetTaskAsync(request, CancellationToken.None); - } - - /// - /// Executes a GET-style request asynchronously, authenticating if needed - /// - /// Target deserialization type - /// Request to be executed - /// The cancellation token - public virtual Task> ExecuteGetTaskAsync(IRestRequest request, CancellationToken token) - { - if (request == null) - { - throw new ArgumentNullException("request"); - } - - request.Method = Method.GET; - return ExecuteTaskAsync(request, token); - } - - /// - /// Executes a POST-style request asynchronously, authenticating if needed - /// - /// Target deserialization type - /// Request to be executed - public virtual Task> ExecutePostTaskAsync(IRestRequest request) - { - return ExecutePostTaskAsync(request, CancellationToken.None); - } - - /// - /// Executes a POST-style request asynchronously, authenticating if needed - /// - /// Target deserialization type - /// Request to be executed - /// The cancellation token - public virtual Task> ExecutePostTaskAsync(IRestRequest request, CancellationToken token) - { - if (request == null) - { - throw new ArgumentNullException("request"); - } - - request.Method = Method.POST; - return ExecuteTaskAsync(request, token); - } - - /// - /// Executes the request asynchronously, authenticating if needed - /// - /// Target deserialization type - /// Request to be executed - public virtual Task> ExecuteTaskAsync(IRestRequest request) - { - return ExecuteTaskAsync(request, CancellationToken.None); - } - - /// - /// Executes the request asynchronously, authenticating if needed - /// - /// Target deserialization type - /// Request to be executed - /// The cancellation token - public virtual Task> ExecuteTaskAsync(IRestRequest request, CancellationToken token) - { - if (request == null) - { - throw new ArgumentNullException("request"); - } - - var taskCompletionSource = new TaskCompletionSource>(); - - try - { - var async = ExecuteAsync(request, (response, _) => - { - if (token.IsCancellationRequested) - { - taskCompletionSource.TrySetCanceled(); - } - else if (response.ErrorException != null) - { - taskCompletionSource.TrySetException(response.ErrorException); - } - else if (response.ResponseStatus != ResponseStatus.Completed) - { - taskCompletionSource.TrySetException(response.ResponseStatus.ToWebException()); - } - else - { - taskCompletionSource.TrySetResult(response); - } - }); - - token.Register(() => - { - async.Abort(); - taskCompletionSource.TrySetCanceled(); - }); - } - catch (Exception ex) - { - taskCompletionSource.TrySetException(ex); - } - - return taskCompletionSource.Task; - } - - /// - /// Executes the request asynchronously, authenticating if needed - /// - /// Request to be executed - public virtual Task ExecuteTaskAsync(IRestRequest request) - { - return ExecuteTaskAsync(request, CancellationToken.None); - } - - /// - /// Executes a GET-style asynchronously, authenticating if needed - /// - /// Request to be executed - public virtual Task ExecuteGetTaskAsync(IRestRequest request) - { - return this.ExecuteGetTaskAsync(request, CancellationToken.None); - } - - /// - /// Executes a GET-style asynchronously, authenticating if needed - /// - /// Request to be executed - /// The cancellation token - public virtual Task ExecuteGetTaskAsync(IRestRequest request, CancellationToken token) - { - if (request == null) - { - throw new ArgumentNullException("request"); - } - - request.Method = Method.GET; - return ExecuteTaskAsync(request, token); - } - - /// - /// Executes a POST-style asynchronously, authenticating if needed - /// - /// Request to be executed - public virtual Task ExecutePostTaskAsync(IRestRequest request) - { - return this.ExecutePostTaskAsync(request, CancellationToken.None); - } - - /// - /// Executes a POST-style asynchronously, authenticating if needed - /// - /// Request to be executed - /// The cancellation token - public virtual Task ExecutePostTaskAsync(IRestRequest request, CancellationToken token) - { - if (request == null) - { - throw new ArgumentNullException("request"); - } - - request.Method = Method.POST; - return ExecuteTaskAsync(request, token); - } - - /// - /// Executes the request asynchronously, authenticating if needed - /// - /// Request to be executed - /// The cancellation token - public virtual Task ExecuteTaskAsync(IRestRequest request, CancellationToken token) - { - if (request == null) - { - throw new ArgumentNullException("request"); - } - - var taskCompletionSource = new TaskCompletionSource(); - - try - { - var async = this.ExecuteAsync(request, (response, _) => - { - if (token.IsCancellationRequested) - { - taskCompletionSource.TrySetCanceled(); - } - else if (response.ErrorException != null) - { - taskCompletionSource.TrySetException(response.ErrorException); - } - else if (response.ResponseStatus != ResponseStatus.Completed) - { - taskCompletionSource.TrySetException(response.ResponseStatus.ToWebException()); - } - else - { - taskCompletionSource.TrySetResult(response); - } - }); - - token.Register(() => - { - async.Abort(); - taskCompletionSource.TrySetCanceled(); - }); - } - catch (Exception ex) - { - taskCompletionSource.TrySetException(ex); - } - - return taskCompletionSource.Task; - } -#endif - } -} \ No newline at end of file diff --git a/RestSharp/RestClient.Sync.cs b/RestSharp/RestClient.Sync.cs deleted file mode 100644 index fc90870f1..000000000 --- a/RestSharp/RestClient.Sync.cs +++ /dev/null @@ -1,118 +0,0 @@ -#if FRAMEWORK -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Text; -using RestSharp.Deserializers; - -namespace RestSharp -{ - public partial class RestClient - { - - - /// - /// Executes the specified request and downloads the response data - /// - /// Request to execute - /// Response data - public byte[] DownloadData(IRestRequest request) - { - var response = Execute(request); - return response.RawBytes; - } - - /// - /// Executes the request and returns a response, authenticating if needed - /// - /// Request to be executed - /// RestResponse - public virtual IRestResponse Execute(IRestRequest request) - { -#if PocketPC - var method = request.Method.ToString(); -#else - var method = Enum.GetName(typeof(Method), request.Method); -#endif - switch (request.Method) - { - case Method.POST: - case Method.PUT: - case Method.PATCH: - return Execute(request, method, DoExecuteAsPost); - default: - return Execute(request, method, DoExecuteAsGet); - } - } - - public IRestResponse ExecuteAsGet(IRestRequest request, string httpMethod) - { - return Execute(request, httpMethod, DoExecuteAsGet); - } - - public IRestResponse ExecuteAsPost(IRestRequest request, string httpMethod) - { - request.Method = Method.POST; // Required by RestClient.BuildUri... - return Execute(request, httpMethod, DoExecuteAsPost); - } - - private IRestResponse Execute(IRestRequest request, string httpMethod, Func getResponse) - { - AuthenticateIfNeeded(this, request); - - IRestResponse response = new RestResponse(); - try - { - var http = HttpFactory.Create(); - - ConfigureHttp(request, http); - - response = ConvertToRestResponse(request, getResponse(http, httpMethod)); - response.Request = request; - response.Request.IncreaseNumAttempts(); - - } - catch (Exception ex) - { - response.ResponseStatus = ResponseStatus.Error; - response.ErrorMessage = ex.Message; - response.ErrorException = ex; - } - - return response; - } - - private static HttpResponse DoExecuteAsGet(IHttp http, string method) - { - return http.AsGet(method); - } - - private static HttpResponse DoExecuteAsPost(IHttp http, string method) - { - return http.AsPost(method); - } - - /// - /// Executes the specified request and deserializes the response content using the appropriate content handler - /// - /// Target deserialization type - /// Request to execute - /// RestResponse[[T]] with deserialized data in Data property - public virtual IRestResponse Execute(IRestRequest request) where T : new() - { - return Deserialize(request, Execute(request)); - } - - public IRestResponse ExecuteAsGet(IRestRequest request, string httpMethod) where T : new() - { - return Deserialize(request, ExecuteAsGet(request, httpMethod)); - } - - public IRestResponse ExecuteAsPost(IRestRequest request, string httpMethod) where T : new() - { - return Deserialize(request, ExecuteAsPost(request, httpMethod)); - } - } -} -#endif \ No newline at end of file diff --git a/RestSharp/RestClient.cs b/RestSharp/RestClient.cs deleted file mode 100644 index 3caf6b022..000000000 --- a/RestSharp/RestClient.cs +++ /dev/null @@ -1,531 +0,0 @@ -#region License -// Copyright 2010 John Sheehan -// -// 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. -#endregion - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Reflection; -using System.Security.Cryptography.X509Certificates; -using System.Text; -using RestSharp.Deserializers; -using RestSharp.Extensions; - -namespace RestSharp -{ - /// - /// Client to translate RestRequests into Http requests and process response result - /// - public partial class RestClient : IRestClient - { - // silverlight friendly way to get current version -#if PocketPC - static readonly Version version = Assembly.GetExecutingAssembly().GetName().Version; -#else - static readonly Version version = new AssemblyName(Assembly.GetExecutingAssembly().FullName).Version; -#endif - public IHttpFactory HttpFactory = new SimpleFactory(); - - /// - /// Default constructor that registers default content handlers - /// - public RestClient() - { -#if WINDOWS_PHONE - UseSynchronizationContext = true; -#endif - ContentHandlers = new Dictionary(); - AcceptTypes = new List(); - DefaultParameters = new List(); - - // register default handlers - AddHandler("application/json", new JsonDeserializer()); - AddHandler("application/xml", new XmlDeserializer()); - AddHandler("text/json", new JsonDeserializer()); - AddHandler("text/x-json", new JsonDeserializer()); - AddHandler("text/javascript", new JsonDeserializer()); - AddHandler("text/xml", new XmlDeserializer()); - AddHandler("*", new XmlDeserializer()); - - FollowRedirects = true; - } - - /// - /// Sets the BaseUrl property for requests made by this client instance - /// - /// - public RestClient(string baseUrl) - : this() - { - BaseUrl = baseUrl; - } - - private IDictionary ContentHandlers { get; set; } - private IList AcceptTypes { get; set; } - - /// - /// Parameters included with every request made with this instance of RestClient - /// If specified in both client and request, the request wins - /// - public IList DefaultParameters { get; private set; } - - /// - /// Registers a content handler to process response content - /// - /// MIME content type of the response content - /// Deserializer to use to process content - public void AddHandler(string contentType, IDeserializer deserializer) - { - ContentHandlers[contentType] = deserializer; - if (contentType != "*") - { - AcceptTypes.Add(contentType); - // add Accept header based on registered deserializers - var accepts = string.Join(", ", AcceptTypes.ToArray()); - this.RemoveDefaultParameter("Accept"); - this.AddDefaultParameter("Accept", accepts, ParameterType.HttpHeader); - } - } - - /// - /// Remove a content handler for the specified MIME content type - /// - /// MIME content type to remove - public void RemoveHandler(string contentType) - { - ContentHandlers.Remove(contentType); - AcceptTypes.Remove(contentType); - this.RemoveDefaultParameter("Accept"); - } - - /// - /// Remove all content handlers - /// - public void ClearHandlers() - { - ContentHandlers.Clear(); - AcceptTypes.Clear(); - this.RemoveDefaultParameter("Accept"); - } - - /// - /// Retrieve the handler for the specified MIME content type - /// - /// MIME content type to retrieve - /// IDeserializer instance - IDeserializer GetHandler(string contentType) - { - if (string.IsNullOrEmpty(contentType) && ContentHandlers.ContainsKey("*")) - { - return ContentHandlers["*"]; - } - - var semicolonIndex = contentType.IndexOf(';'); - if (semicolonIndex > -1) contentType = contentType.Substring(0, semicolonIndex); - IDeserializer handler = null; - if (ContentHandlers.ContainsKey(contentType)) - { - handler = ContentHandlers[contentType]; - } - else if (ContentHandlers.ContainsKey("*")) - { - handler = ContentHandlers["*"]; - } - - return handler; - } - - /// - /// Maximum number of redirects to follow if FollowRedirects is true - /// - public int? MaxRedirects { get; set; } - -#if FRAMEWORK - /// - /// X509CertificateCollection to be sent with request - /// - public X509CertificateCollection ClientCertificates { get; set; } - - /// - /// Proxy to use for requests made by this client instance. - /// Passed on to underlying WebRequest if set. - /// - public IWebProxy Proxy { get; set; } -#endif - - /// - /// Default is true. Determine whether or not requests that result in - /// HTTP status codes of 3xx should follow returned redirect - /// - public bool FollowRedirects { get; set; } - - /// - /// The CookieContainer used for requests made by this client instance - /// -#if !PocketPC - public CookieContainer CookieContainer { get; set; } -#endif - - /// - /// UserAgent to use for requests made by this client instance - /// - public string UserAgent { get; set; } - - /// - /// Timeout in milliseconds to use for requests made by this client instance - /// - public int Timeout { get; set; } - - /// - /// The number of milliseconds before the writing or reading times out. - /// - public int ReadWriteTimeout { get; set; } - - /// - /// Whether to invoke async callbacks using the SynchronizationContext.Current captured when invoked - /// - public bool UseSynchronizationContext { get; set; } - - /// - /// Authenticator to use for requests made by this client instance - /// - public IAuthenticator Authenticator { get; set; } - - private string _baseUrl; - /// - /// Combined with Request.Resource to construct URL for request - /// Should include scheme and domain without trailing slash. - /// - /// - /// client.BaseUrl = "http://example.com"; - /// - public virtual string BaseUrl - { - get - { - return _baseUrl; - } - set - { - _baseUrl = value; - if (_baseUrl != null && _baseUrl.EndsWith("/")) - { - _baseUrl = _baseUrl.Substring(0, _baseUrl.Length - 1); - } - } - } - - public bool PreAuthenticate { get; set; } - - private void AuthenticateIfNeeded(RestClient client, IRestRequest request) - { - if (Authenticator != null) - { - Authenticator.Authenticate(client, request); - } - } - - /// - /// Assembles URL to call based on parameters, method and resource - /// - /// RestRequest to execute - /// Assembled System.Uri - public Uri BuildUri(IRestRequest request) - { - var assembled = request.Resource; - var urlParms = request.Parameters.Where(p => p.Type == ParameterType.UrlSegment); - foreach (var p in urlParms) - { - assembled = assembled.Replace("{" + p.Name + "}", p.Value.ToString().UrlEncode()); - } - - if (!string.IsNullOrEmpty(assembled) && assembled.StartsWith("/")) - { - assembled = assembled.Substring(1); - } - - if (!string.IsNullOrEmpty(BaseUrl)) - { - if (string.IsNullOrEmpty(assembled)) - { - assembled = BaseUrl; - } - else - { - assembled = string.Format("{0}/{1}", BaseUrl, assembled); - } - } - - IEnumerable parameters = null; - - if (request.Method != Method.POST && request.Method != Method.PUT && request.Method != Method.PATCH) - { - // build and attach querystring if this is a get-style request - parameters = request.Parameters.Where(p => p.Type == ParameterType.GetOrPost || p.Type == ParameterType.QueryString); - } - else - { - parameters = request.Parameters.Where(p => p.Type == ParameterType.QueryString); - } - - // build and attach querystring - if (parameters != null && parameters.Any()) - { - var data = EncodeParameters(parameters); - assembled = string.Format("{0}?{1}", assembled, data); - } - - return new Uri(assembled); - } - - private static string EncodeParameters(IEnumerable parameters) - { - var querystring = new StringBuilder(); - foreach (var p in parameters) - { - if (querystring.Length > 1) - querystring.Append("&"); - querystring.AppendFormat("{0}={1}", p.Name.UrlEncode(), (p.Value.ToString()).UrlEncode()); - } - - return querystring.ToString(); - } - - private void ConfigureHttp(IRestRequest request, IHttp http) - { - http.AlwaysMultipartFormData = request.AlwaysMultipartFormData; -#if !PocketPC - http.UseDefaultCredentials = request.UseDefaultCredentials; -#endif - http.ResponseWriter = request.ResponseWriter; -#if !PocketPC - http.CookieContainer = CookieContainer; -#endif - // move RestClient.DefaultParameters into Request.Parameters - foreach (var p in DefaultParameters) - { - if (request.Parameters.Any(p2 => p2.Name == p.Name && p2.Type == p.Type)) - { - continue; - } - - request.AddParameter(p); - } - - // Add Accept header based on registered deserializers if none has been set by the caller. -#if PocketPC - if (request.Parameters.All(p2 => p2.Name.ToLower() != "accept")) -#else - if (request.Parameters.All(p2 => p2.Name.ToLowerInvariant() != "accept")) -#endif - { - var accepts = string.Join(", ", AcceptTypes.ToArray()); - request.AddParameter("Accept", accepts, ParameterType.HttpHeader); - } - - http.Url = BuildUri(request); - http.PreAuthenticate = PreAuthenticate; - - var userAgent = UserAgent ?? http.UserAgent; - http.UserAgent = userAgent.HasValue() ? userAgent : "RestSharp/" + version; - - var timeout = request.Timeout > 0 ? request.Timeout : Timeout; - if (timeout > 0) - { - http.Timeout = timeout; - } - - var readWriteTimeout = request.ReadWriteTimeout > 0 ? request.ReadWriteTimeout : ReadWriteTimeout; - if (readWriteTimeout > 0) - { - http.ReadWriteTimeout = readWriteTimeout; - } - -#if !SILVERLIGHT - http.FollowRedirects = FollowRedirects; -#endif -#if FRAMEWORK - if (ClientCertificates != null) - { - http.ClientCertificates = ClientCertificates; - } - - http.MaxRedirects = MaxRedirects; -#endif - - if (request.Credentials != null) - { - http.Credentials = request.Credentials; - } - - var headers = from p in request.Parameters - where p.Type == ParameterType.HttpHeader - select new HttpHeader - { - Name = p.Name, - Value = p.Value.ToString() - }; - - foreach (var header in headers) - { - http.Headers.Add(header); - } - - var cookies = from p in request.Parameters - where p.Type == ParameterType.Cookie - select new HttpCookie - { - Name = p.Name, - Value = p.Value.ToString() - }; - - foreach (var cookie in cookies) - { - http.Cookies.Add(cookie); - } - - var @params = from p in request.Parameters - where p.Type == ParameterType.GetOrPost - && p.Value != null - select new HttpParameter - { - Name = p.Name, - Value = p.Value.ToString() - }; - - foreach (var parameter in @params) - { - http.Parameters.Add(parameter); - } - - foreach (var file in request.Files) - { - http.Files.Add(new HttpFile { Name = file.Name, ContentType = file.ContentType, Writer = file.Writer, FileName = file.FileName, ContentLength = file.ContentLength }); - } - - var body = (from p in request.Parameters - where p.Type == ParameterType.RequestBody - select p).FirstOrDefault(); - - if (body != null) - { - object val = body.Value; - if (val is byte[]) - http.RequestBodyBytes = (byte[])val; - else - http.RequestBody = body.Value.ToString(); - http.RequestContentType = body.Name; - } -#if FRAMEWORK - ConfigureProxy(http); -#endif - } - -#if FRAMEWORK - private void ConfigureProxy(IHttp http) - { - if (Proxy != null) - { - http.Proxy = Proxy; - } - } -#endif - - private RestResponse ConvertToRestResponse(IRestRequest request, HttpResponse httpResponse) - { - var restResponse = new RestResponse(); - restResponse.Content = httpResponse.Content; - restResponse.ContentEncoding = httpResponse.ContentEncoding; - restResponse.ContentLength = httpResponse.ContentLength; - restResponse.ContentType = httpResponse.ContentType; - restResponse.ErrorException = httpResponse.ErrorException; - restResponse.ErrorMessage = httpResponse.ErrorMessage; - restResponse.RawBytes = httpResponse.RawBytes; - restResponse.ResponseStatus = httpResponse.ResponseStatus; - restResponse.ResponseUri = httpResponse.ResponseUri; - restResponse.Server = httpResponse.Server; - restResponse.StatusCode = httpResponse.StatusCode; - restResponse.StatusDescription = httpResponse.StatusDescription; - restResponse.Request = request; - - foreach (var header in httpResponse.Headers) - { - restResponse.Headers.Add(new Parameter { Name = header.Name, Value = header.Value, Type = ParameterType.HttpHeader }); - } - - foreach (var cookie in httpResponse.Cookies) - { - restResponse.Cookies.Add(new RestResponseCookie - { - Comment = cookie.Comment, - CommentUri = cookie.CommentUri, - Discard = cookie.Discard, - Domain = cookie.Domain, - Expired = cookie.Expired, - Expires = cookie.Expires, - HttpOnly = cookie.HttpOnly, - Name = cookie.Name, - Path = cookie.Path, - Port = cookie.Port, - Secure = cookie.Secure, - TimeStamp = cookie.TimeStamp, - Value = cookie.Value, - Version = cookie.Version - }); - } - - return restResponse; - } - - private IRestResponse Deserialize(IRestRequest request, IRestResponse raw) - { - request.OnBeforeDeserialization(raw); - - IRestResponse response = new RestResponse(); - try - { - response = raw.toAsyncResponse(); - response.Request = request; - - // Only attempt to deserialize if the request has not errored due - // to a transport or framework exception. HTTP errors should attempt to - // be deserialized - if (response.ErrorException==null) - { - IDeserializer handler = GetHandler(raw.ContentType); - // Only continue if there is a handler defined else there is no way to deserialize the data. - // This can happen when a request returns for example a 404 page instead of the requested JSON/XML resource - if (handler != null) - { - handler.RootElement = request.RootElement; - handler.DateFormat = request.DateFormat; - handler.Namespace = request.XmlNamespace; - - response.Data = handler.Deserialize(raw); - } - } - } - catch (Exception ex) - { - response.ResponseStatus = ResponseStatus.Error; - response.ErrorMessage = ex.Message; - response.ErrorException = ex; - } - - return response; - } - } -} diff --git a/RestSharp/RestClientExtensions.cs b/RestSharp/RestClientExtensions.cs deleted file mode 100644 index c0c29efde..000000000 --- a/RestSharp/RestClientExtensions.cs +++ /dev/null @@ -1,333 +0,0 @@ -using System; -using System.Linq; -#if NET4 -using System.Threading.Tasks; -#endif - -namespace RestSharp -{ - public static partial class RestClientExtensions - { - /// - /// Executes the request and callback asynchronously, authenticating if needed - /// - /// The IRestClient this method extends - /// Request to be executed - /// Callback function to be executed upon completion - public static RestRequestAsyncHandle ExecuteAsync(this IRestClient client, IRestRequest request, Action callback) - { - return client.ExecuteAsync(request, (response, handle) => callback(response)); - } - - /// - /// Executes the request and callback asynchronously, authenticating if needed - /// - /// The IRestClient this method extends - /// Target deserialization type - /// Request to be executed - /// Callback function to be executed upon completion providing access to the async handle - public static RestRequestAsyncHandle ExecuteAsync(this IRestClient client, IRestRequest request, Action> callback) where T : new() - { - return client.ExecuteAsync(request, (response, asyncHandle) => callback(response)); - } - - public static RestRequestAsyncHandle GetAsync(this IRestClient client, IRestRequest request, Action, RestRequestAsyncHandle> callback) where T : new() - { - request.Method = Method.GET; - return client.ExecuteAsync(request, callback); - } - - public static RestRequestAsyncHandle PostAsync(this IRestClient client, IRestRequest request, Action, RestRequestAsyncHandle> callback) where T : new() - { - request.Method = Method.POST; - return client.ExecuteAsync(request, callback); - } - - public static RestRequestAsyncHandle PutAsync(this IRestClient client, IRestRequest request, Action, RestRequestAsyncHandle> callback) where T : new() - { - request.Method = Method.PUT; - return client.ExecuteAsync(request, callback); - } - - public static RestRequestAsyncHandle HeadAsync(this IRestClient client, IRestRequest request, Action, RestRequestAsyncHandle> callback) where T : new() - { - request.Method = Method.HEAD; - return client.ExecuteAsync(request, callback); - } - - public static RestRequestAsyncHandle OptionsAsync(this IRestClient client, IRestRequest request, Action, RestRequestAsyncHandle> callback) where T : new() - { - request.Method = Method.OPTIONS; - return client.ExecuteAsync(request, callback); - } - - public static RestRequestAsyncHandle PatchAsync(this IRestClient client, IRestRequest request, Action, RestRequestAsyncHandle> callback) where T : new() - { - request.Method = Method.PATCH; - return client.ExecuteAsync(request, callback); - } - - public static RestRequestAsyncHandle DeleteAsync(this IRestClient client, IRestRequest request, Action, RestRequestAsyncHandle> callback) where T : new() - { - request.Method = Method.DELETE; - return client.ExecuteAsync(request, callback); - } - - public static RestRequestAsyncHandle GetAsync(this IRestClient client, IRestRequest request, Action callback) - { - request.Method = Method.GET; - return client.ExecuteAsync(request, callback); - } - - public static RestRequestAsyncHandle PostAsync(this IRestClient client, IRestRequest request, Action callback) - { - request.Method = Method.POST; - return client.ExecuteAsync(request, callback); - } - - public static RestRequestAsyncHandle PutAsync(this IRestClient client, IRestRequest request, Action callback) - { - request.Method = Method.PUT; - return client.ExecuteAsync(request, callback); - } - - public static RestRequestAsyncHandle HeadAsync(this IRestClient client, IRestRequest request, Action callback) - { - request.Method = Method.HEAD; - return client.ExecuteAsync(request, callback); - } - - public static RestRequestAsyncHandle OptionsAsync(this IRestClient client, IRestRequest request, Action callback) - { - request.Method = Method.OPTIONS; - return client.ExecuteAsync(request, callback); - } - - public static RestRequestAsyncHandle PatchAsync(this IRestClient client, IRestRequest request, Action callback) - { - request.Method = Method.PATCH; - return client.ExecuteAsync(request, callback); - } - - public static RestRequestAsyncHandle DeleteAsync(this IRestClient client, IRestRequest request, Action callback) - { - request.Method = Method.DELETE; - return client.ExecuteAsync(request, callback); - } - -#if NET4 - public static Task GetTaskAsync(this IRestClient client, IRestRequest request) where T : new() - { - return client.ExecuteGetTaskAsync(request).ContinueWith(x => x.Result.Data); - } - - public static Task PostTaskAsync(this IRestClient client, IRestRequest request) where T : new() - { - return client.ExecutePostTaskAsync(request).ContinueWith(x => x.Result.Data); - } - - public static Task PutTaskAsync(this IRestClient client, IRestRequest request) where T : new() - { - request.Method = Method.PUT; - return client.ExecuteTaskAsync(request).ContinueWith(x => x.Result.Data); - } - - public static Task HeadTaskAsync(this IRestClient client, IRestRequest request) where T : new() - { - request.Method = Method.HEAD; - return client.ExecuteTaskAsync(request).ContinueWith(x => x.Result.Data); - } - - public static Task OptionsTaskAsync(this IRestClient client, IRestRequest request) where T : new() - { - request.Method = Method.OPTIONS; - return client.ExecuteTaskAsync(request).ContinueWith(x => x.Result.Data); - } - - public static Task PatchTaskAsync(this IRestClient client, IRestRequest request) where T : new() - { - request.Method = Method.PATCH; - return client.ExecuteTaskAsync(request).ContinueWith(x => x.Result.Data); - } - - public static Task DeleteTaskAsync(this IRestClient client, IRestRequest request) where T : new() - { - request.Method = Method.DELETE; - return client.ExecuteTaskAsync(request).ContinueWith(x => x.Result.Data); - } -#endif - -#if FRAMEWORK - public static IRestResponse Get(this IRestClient client, IRestRequest request) where T : new() - { - request.Method = Method.GET; - return client.Execute(request); - } - - public static IRestResponse Post(this IRestClient client, IRestRequest request) where T : new() - { - request.Method = Method.POST; - return client.Execute(request); - } - - public static IRestResponse Put(this IRestClient client, IRestRequest request) where T : new() - { - request.Method = Method.PUT; - return client.Execute(request); - } - - public static IRestResponse Head(this IRestClient client, IRestRequest request) where T : new() - { - request.Method = Method.HEAD; - return client.Execute(request); - } - - public static IRestResponse Options(this IRestClient client, IRestRequest request) where T : new() - { - request.Method = Method.OPTIONS; - return client.Execute(request); - } - - public static IRestResponse Patch(this IRestClient client, IRestRequest request) where T : new() - { - request.Method = Method.PATCH; - return client.Execute(request); - } - - public static IRestResponse Delete(this IRestClient client, IRestRequest request) where T : new() - { - request.Method = Method.DELETE; - return client.Execute(request); - } - - public static IRestResponse Get(this IRestClient client, IRestRequest request) - { - request.Method = Method.GET; - return client.Execute(request); - } - - public static IRestResponse Post(this IRestClient client, IRestRequest request) - { - request.Method = Method.POST; - return client.Execute(request); - } - - public static IRestResponse Put(this IRestClient client, IRestRequest request) - { - request.Method = Method.PUT; - return client.Execute(request); - } - - public static IRestResponse Head(this IRestClient client, IRestRequest request) - { - request.Method = Method.HEAD; - return client.Execute(request); - } - - public static IRestResponse Options(this IRestClient client, IRestRequest request) - { - request.Method = Method.OPTIONS; - return client.Execute(request); - } - - public static IRestResponse Patch(this IRestClient client, IRestRequest request) - { - request.Method = Method.PATCH; - return client.Execute(request); - } - - public static IRestResponse Delete(this IRestClient client, IRestRequest request) - { - request.Method = Method.DELETE; - return client.Execute(request); - } -#endif - - /// - /// Add a parameter to use on every request made with this client instance - /// - /// The IRestClient instance - /// Parameter to add - /// - public static void AddDefaultParameter(this IRestClient restClient, Parameter p) - { - if (p.Type == ParameterType.RequestBody) - { - throw new NotSupportedException( - "Cannot set request body from default headers. Use Request.AddBody() instead."); - } - - restClient.DefaultParameters.Add(p); - } - - /// - /// Removes a parameter from the default parameters that are used on every request made with this client instance - /// - /// The IRestClient instance - /// The name of the parameter that needs to be removed - /// - public static void RemoveDefaultParameter(this IRestClient restClient, string name) - { - var parameter = restClient.DefaultParameters.SingleOrDefault(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); - if (parameter != null) - { - restClient.DefaultParameters.Remove(parameter); - } - } - - /// - /// Adds a HTTP parameter (QueryString for GET, DELETE, OPTIONS and HEAD; Encoded form for POST and PUT) - /// Used on every request made by this client instance - /// - /// The IRestClient instance - /// Name of the parameter - /// Value of the parameter - /// This request - public static void AddDefaultParameter(this IRestClient restClient, string name, object value) - { - restClient.AddDefaultParameter(new Parameter { Name = name, Value = value, Type = ParameterType.GetOrPost }); - } - - /// - /// Adds a parameter to the request. There are four types of parameters: - /// - GetOrPost: Either a QueryString value or encoded form value based on method - /// - HttpHeader: Adds the name/value pair to the HTTP request's Headers collection - /// - UrlSegment: Inserted into URL if there is a matching url token e.g. {AccountId} - /// - RequestBody: Used by AddBody() (not recommended to use directly) - /// - /// The IRestClient instance - /// Name of the parameter - /// Value of the parameter - /// The type of parameter to add - /// This request - public static void AddDefaultParameter(this IRestClient restClient, string name, object value, ParameterType type) - { - restClient.AddDefaultParameter(new Parameter { Name = name, Value = value, Type = type }); - } - - /// - /// Shortcut to AddDefaultParameter(name, value, HttpHeader) overload - /// - /// The IRestClient instance - /// Name of the header to add - /// Value of the header to add - /// - public static void AddDefaultHeader(this IRestClient restClient, string name, string value) - { - restClient.AddDefaultParameter(name, value, ParameterType.HttpHeader); - } - - /// - /// Shortcut to AddDefaultParameter(name, value, UrlSegment) overload - /// - /// The IRestClient instance - /// Name of the segment to add - /// Value of the segment to add - /// - public static void AddDefaultUrlSegment(this IRestClient restClient, string name, string value) - { - restClient.AddDefaultParameter(name, value, ParameterType.UrlSegment); - } - - } -} \ No newline at end of file diff --git a/RestSharp/RestRequest.cs b/RestSharp/RestRequest.cs deleted file mode 100644 index 2cbf2ee8b..000000000 --- a/RestSharp/RestRequest.cs +++ /dev/null @@ -1,501 +0,0 @@ -#region License -// Copyright 2010 John Sheehan -// -// 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. -#endregion - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using RestSharp.Extensions; -using RestSharp.Serializers; - -namespace RestSharp -{ - /// - /// Container for data used to make requests - /// - public class RestRequest : IRestRequest - { - /// - /// Always send a multipart/form-data request - even when no Files are present. - /// - public bool AlwaysMultipartFormData { get; set; } - - /// - /// Serializer to use when writing JSON request bodies. Used if RequestFormat is Json. - /// By default the included JsonSerializer is used (currently using JSON.NET default serialization). - /// - public ISerializer JsonSerializer { get; set; } - - /// - /// Serializer to use when writing XML request bodies. Used if RequestFormat is Xml. - /// By default the included XmlSerializer is used. - /// - public ISerializer XmlSerializer { get; set; } - - /// - /// Set this to write response to Stream rather than reading into memory. - /// - public Action ResponseWriter { get; set; } - - /// - /// Determine whether or not the "default credentials" (e.g. the user account under which the current process is running) - /// will be sent along to the server. The default is false. - /// - public bool UseDefaultCredentials { get; set; } - - /// - /// Default constructor - /// - public RestRequest() - { - Parameters = new List(); - Files = new List(); - XmlSerializer = new XmlSerializer(); - JsonSerializer = new JsonSerializer(); - - OnBeforeDeserialization = r => { }; - } - - /// - /// Sets Method property to value of method - /// - /// Method to use for this request - public RestRequest(Method method) - : this() - { - Method = method; - } - - /// - /// Sets Resource property - /// - /// Resource to use for this request - public RestRequest(string resource) - : this(resource, Method.GET) - { - } - - /// - /// Sets Resource and Method properties - /// - /// Resource to use for this request - /// Method to use for this request - public RestRequest(string resource, Method method) - : this() - { - Resource = resource; - Method = method; - } - - - /// - /// Sets Resource property - /// - /// Resource to use for this request - public RestRequest(Uri resource) - : this(resource, Method.GET) - { - - } - - /// - /// Sets Resource and Method properties - /// - /// Resource to use for this request - /// Method to use for this request - public RestRequest(Uri resource, Method method) - : this(resource.IsAbsoluteUri ? resource.AbsolutePath + resource.Query : resource.OriginalString, method) - { - //resource.PathAndQuery not supported by Silverlight :( - } - - /// - /// Adds a file to the Files collection to be included with a POST or PUT request - /// (other methods do not support file uploads). - /// - /// The parameter name to use in the request - /// Full path to file to upload - /// This request - public IRestRequest AddFile (string name, string path) - { - FileInfo f = new FileInfo (path); - long fileLength = f.Length; - return AddFile(new FileParameter - { - Name = name, - FileName = Path.GetFileName(path), - ContentLength = fileLength, - Writer = s => - { - using(var file = new StreamReader(path)) - { - file.BaseStream.CopyTo(s); - } - } - }); - } - - /// - /// Adds the bytes to the Files collection with the specified file name - /// - /// The parameter name to use in the request - /// The file data - /// The file name to use for the uploaded file - /// This request - public IRestRequest AddFile (string name, byte[] bytes, string fileName) - { - return AddFile(FileParameter.Create(name, bytes, fileName)); - } - - /// - /// Adds the bytes to the Files collection with the specified file name and content type - /// - /// The parameter name to use in the request - /// The file data - /// The file name to use for the uploaded file - /// The MIME type of the file to upload - /// This request - public IRestRequest AddFile (string name, byte[] bytes, string fileName, string contentType) - { - return AddFile(FileParameter.Create(name, bytes, fileName, contentType)); - } - - /// - /// Adds the bytes to the Files collection with the specified file name and content type - /// - /// The parameter name to use in the request - /// A function that writes directly to the stream. Should NOT close the stream. - /// The file name to use for the uploaded file - /// This request - public IRestRequest AddFile (string name, Action writer, string fileName) - { - return AddFile(name, writer, fileName, null); - } - - /// - /// Adds the bytes to the Files collection with the specified file name and content type - /// - /// The parameter name to use in the request - /// A function that writes directly to the stream. Should NOT close the stream. - /// The file name to use for the uploaded file - /// The MIME type of the file to upload - /// This request - public IRestRequest AddFile (string name, Action writer, string fileName, string contentType) - { - return AddFile(new FileParameter { Name = name, Writer = writer, FileName = fileName, ContentType = contentType }); - } - - private IRestRequest AddFile (FileParameter file) - { - Files.Add(file); - return this; - } - - /// - /// Serializes obj to format specified by RequestFormat, but passes xmlNamespace if using the default XmlSerializer - /// - /// The object to serialize - /// The XML namespace to use when serializing - /// This request - public IRestRequest AddBody (object obj, string xmlNamespace) - { - string serialized; - string contentType; - - switch (RequestFormat) - { - case DataFormat.Json: - serialized = JsonSerializer.Serialize(obj); - contentType = JsonSerializer.ContentType; - break; - - case DataFormat.Xml: - XmlSerializer.Namespace = xmlNamespace; - serialized = XmlSerializer.Serialize(obj); - contentType = XmlSerializer.ContentType; - break; - - default: - serialized = ""; - contentType = ""; - break; - } - - // passing the content type as the parameter name because there can only be - // one parameter with ParameterType.RequestBody so name isn't used otherwise - // it's a hack, but it works :) - return AddParameter(contentType, serialized, ParameterType.RequestBody); - } - - /// - /// Serializes obj to data format specified by RequestFormat and adds it to the request body. - /// - /// The object to serialize - /// This request - public IRestRequest AddBody (object obj) - { - return AddBody(obj, ""); - } - - /// - /// Calls AddParameter() for all public, readable properties specified in the white list - /// - /// - /// request.AddObject(product, "ProductId", "Price", ...); - /// - /// The object with properties to add as parameters - /// The names of the properties to include - /// This request - public IRestRequest AddObject (object obj, params string[] whitelist) - { - // automatically create parameters from object props - var type = obj.GetType(); - var props = type.GetProperties(); - - foreach (var prop in props) - { - bool isAllowed = whitelist.Length == 0 || (whitelist.Length > 0 && whitelist.Contains(prop.Name)); - - if (isAllowed) - { - var propType = prop.PropertyType; - var val = prop.GetValue(obj, null); - - if (val != null) - { - if (propType.IsArray) - { - var elementType = propType.GetElementType(); - - if (((Array)val).Length > 0 && (elementType.IsPrimitive || elementType.IsValueType || elementType == typeof(string))) { - // convert the array to an array of strings - var values = (from object item in ((Array)val) select item.ToString()).ToArray(); - val = string.Join(",", values); - } else { - // try to cast it - val = string.Join(",", (string[])val); - } - } - - AddParameter(prop.Name, val); - } - } - } - - return this; - } - - /// - /// Calls AddParameter() for all public, readable properties of obj - /// - /// The object with properties to add as parameters - /// This request - public IRestRequest AddObject (object obj) - { - AddObject(obj, new string[] { }); - return this; - } - - /// - /// Add the parameter to the request - /// - /// Parameter to add - /// - public IRestRequest AddParameter (Parameter p) - { - Parameters.Add(p); - return this; - } - - /// - /// Adds a HTTP parameter to the request (QueryString for GET, DELETE, OPTIONS and HEAD; Encoded form for POST and PUT) - /// - /// Name of the parameter - /// Value of the parameter - /// This request - public IRestRequest AddParameter (string name, object value) - { - return AddParameter(new Parameter { Name = name, Value = value, Type = ParameterType.GetOrPost }); - } - - /// - /// Adds a parameter to the request. There are four types of parameters: - /// - GetOrPost: Either a QueryString value or encoded form value based on method - /// - HttpHeader: Adds the name/value pair to the HTTP request's Headers collection - /// - UrlSegment: Inserted into URL if there is a matching url token e.g. {AccountId} - /// - RequestBody: Used by AddBody() (not recommended to use directly) - /// - /// Name of the parameter - /// Value of the parameter - /// The type of parameter to add - /// This request - public IRestRequest AddParameter (string name, object value, ParameterType type) - { - return AddParameter(new Parameter { Name = name, Value = value, Type = type }); - } - - /// - /// Shortcut to AddParameter(name, value, HttpHeader) overload - /// - /// Name of the header to add - /// Value of the header to add - /// - public IRestRequest AddHeader (string name, string value) - { - return AddParameter(name, value, ParameterType.HttpHeader); - } - - /// - /// Shortcut to AddParameter(name, value, Cookie) overload - /// - /// Name of the cookie to add - /// Value of the cookie to add - /// - public IRestRequest AddCookie (string name, string value) - { - return AddParameter(name, value, ParameterType.Cookie); - } - - /// - /// Shortcut to AddParameter(name, value, UrlSegment) overload - /// - /// Name of the segment to add - /// Value of the segment to add - /// - public IRestRequest AddUrlSegment (string name, string value) - { - return AddParameter(name, value, ParameterType.UrlSegment); - } - - /// - /// Container of all HTTP parameters to be passed with the request. - /// See AddParameter() for explanation of the types of parameters that can be passed - /// - public List Parameters { get; private set; } - - /// - /// Container of all the files to be uploaded with the request. - /// - public List Files { get; private set; } - - private Method _method = Method.GET; - /// - /// Determines what HTTP method to use for this request. Supported methods: GET, POST, PUT, DELETE, HEAD, OPTIONS - /// Default is GET - /// - public Method Method - { - get { return _method; } - set { _method = value; } - } - - /// - /// The Resource URL to make the request against. - /// Tokens are substituted with UrlSegment parameters and match by name. - /// Should not include the scheme or domain. Do not include leading slash. - /// Combined with RestClient.BaseUrl to assemble final URL: - /// {BaseUrl}/{Resource} (BaseUrl is scheme + domain, e.g. http://example.com) - /// - /// - /// // example for url token replacement - /// request.Resource = "Products/{ProductId}"; - /// request.AddParameter("ProductId", 123, ParameterType.UrlSegment); - /// - public string Resource { get; set; } - - private DataFormat _requestFormat = DataFormat.Xml; - /// - /// Serializer to use when writing XML request bodies. Used if RequestFormat is Xml. - /// By default XmlSerializer is used. - /// - public DataFormat RequestFormat - { - get - { - return _requestFormat; - } - set - { - _requestFormat = value; - } - } - - /// - /// Used by the default deserializers to determine where to start deserializing from. - /// Can be used to skip container or root elements that do not have corresponding deserialzation targets. - /// - public string RootElement { get; set; } - - /// - /// A function to run prior to deserializing starting (e.g. change settings if error encountered) - /// - public Action OnBeforeDeserialization { get; set; } - - /// - /// Used by the default deserializers to explicitly set which date format string to use when parsing dates. - /// - public string DateFormat { get; set; } - - /// - /// Used by XmlDeserializer. If not specified, XmlDeserializer will flatten response by removing namespaces from element names. - /// - public string XmlNamespace { get; set; } - - /// - /// In general you would not need to set this directly. Used by the NtlmAuthenticator. - /// - public ICredentials Credentials { get; set; } - - /// - /// Gets or sets a user-defined state object that contains information about a request and which can be later - /// retrieved when the request completes. - /// - public object UserState { get; set; } - - /// - /// Timeout in milliseconds to be used for the request. This timeout value overrides a timeout set on the RestClient. - /// - public int Timeout { get; set; } - - /// - /// The number of milliseconds before the writing or reading times out. This timeout value overrides a timeout set on the RestClient. - /// - public int ReadWriteTimeout { get; set; } - - private int _attempts; - - /// - /// Internal Method so that RestClient can increase the number of attempts - /// - public void IncreaseNumAttempts() - { - _attempts++; - } - - /// - /// How many attempts were made to send this Request? - /// - /// - /// This Number is incremented each time the RestClient sends the request. - /// Useful when using Asynchronous Execution with Callbacks - /// - public int Attempts - { - get { return _attempts; } - } - } -} diff --git a/RestSharp/RestRequestAsyncHandle.cs b/RestSharp/RestRequestAsyncHandle.cs deleted file mode 100644 index 7e1c5650b..000000000 --- a/RestSharp/RestRequestAsyncHandle.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using System.Net; -namespace RestSharp -{ - public class RestRequestAsyncHandle - { - public HttpWebRequest WebRequest; - - public RestRequestAsyncHandle() - { - } - - public RestRequestAsyncHandle(HttpWebRequest webRequest) - { - WebRequest = webRequest; - } - - public void Abort() - { - if (WebRequest != null) - WebRequest.Abort(); - } - } -} - diff --git a/RestSharp/RestResponse.cs b/RestSharp/RestResponse.cs deleted file mode 100644 index 80bf41761..000000000 --- a/RestSharp/RestResponse.cs +++ /dev/null @@ -1,174 +0,0 @@ -#region License -// Copyright 2010 John Sheehan -// -// 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. -#endregion - -using System; -using System.Collections.Generic; -using System.Net; -using RestSharp.Extensions; - -namespace RestSharp -{ - /// - /// Base class for common properties shared by RestResponse and RestResponse[[T]] - /// - public abstract class RestResponseBase - { - private string _content; - - /// - /// Default constructor - /// - public RestResponseBase() - { - Headers = new List(); - Cookies = new List(); - } - /// - /// The RestRequest that was made to get this RestResponse - /// - /// - /// Mainly for debugging if ResponseStatus is not OK - /// - public IRestRequest Request { get; set; } - /// - /// MIME content type of response - /// - public string ContentType { get; set; } - /// - /// Length in bytes of the response content - /// - public long ContentLength { get; set; } - /// - /// Encoding of the response content - /// - public string ContentEncoding { get; set; } - /// - /// String representation of response content - /// - public string Content - { - get - { - if (_content == null) - { - _content = RawBytes.AsString(); - } - return _content; - } - set - { - _content = value; - } - } - /// - /// HTTP response status code - /// - public HttpStatusCode StatusCode { get; set; } - /// - /// Description of HTTP status returned - /// - public string StatusDescription { get; set; } - /// - /// Response content - /// - public byte[] RawBytes { get; set; } - /// - /// The URL that actually responded to the content (different from request if redirected) - /// - public Uri ResponseUri { get; set; } - /// - /// HttpWebResponse.Server - /// - public string Server { get; set; } - /// - /// Cookies returned by server with the response - /// - public IList Cookies { get; protected internal set; } - /// - /// Headers returned by server with the response - /// - public IList Headers { get; protected internal set; } - - private ResponseStatus _responseStatus = ResponseStatus.None; - /// - /// Status of the request. Will return Error for transport errors. - /// HTTP errors will still return ResponseStatus.Completed, check StatusCode instead - /// - public ResponseStatus ResponseStatus - { - get - { - return _responseStatus; - } - set - { - _responseStatus = value; - } - } - - /// - /// Transport or other non-HTTP error generated while attempting request - /// - public string ErrorMessage { get; set; } - - /// - /// The exception thrown during the request, if any - /// - public Exception ErrorException { get; set; } - } - - /// - /// Container for data sent back from API including deserialized data - /// - /// Type of data to deserialize to - public class RestResponse : RestResponseBase, IRestResponse - { - /// - /// Deserialized entity data - /// - public T Data { get; set; } - - public static explicit operator RestResponse(RestResponse response) - { - return new RestResponse - { - ContentEncoding = response.ContentEncoding, - ContentLength = response.ContentLength, - ContentType = response.ContentType, - Cookies = response.Cookies, - ErrorMessage = response.ErrorMessage, - ErrorException = response.ErrorException, - Headers = response.Headers, - RawBytes = response.RawBytes, - ResponseStatus = response.ResponseStatus, - ResponseUri = response.ResponseUri, - Server = response.Server, - StatusCode = response.StatusCode, - StatusDescription = response.StatusDescription, - Request = response.Request - }; - } - - } - - /// - /// Container for data sent back from API - /// - public class RestResponse : RestResponseBase, IRestResponse - { - - } -} diff --git a/RestSharp/RestResponseCookie.cs b/RestSharp/RestResponseCookie.cs deleted file mode 100644 index 5d42c2b44..000000000 --- a/RestSharp/RestResponseCookie.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System; -namespace RestSharp -{ - public class RestResponseCookie - { - /// - /// Comment of the cookie - /// - public string Comment { get; set; } - /// - /// Comment of the cookie - /// - public Uri CommentUri { get; set; } - /// - /// Indicates whether the cookie should be discarded at the end of the session - /// - public bool Discard { get; set; } - /// - /// Domain of the cookie - /// - public string Domain { get; set; } - /// - /// Indicates whether the cookie is expired - /// - public bool Expired { get; set; } - /// - /// Date and time that the cookie expires - /// - public DateTime Expires { get; set; } - /// - /// Indicates that this cookie should only be accessed by the server - /// - public bool HttpOnly { get; set; } - /// - /// Name of the cookie - /// - public string Name { get; set; } - /// - /// Path of the cookie - /// - public string Path { get; set; } - /// - /// Port of the cookie - /// - public string Port { get; set; } - /// - /// Indicates that the cookie should only be sent over secure channels - /// - public bool Secure { get; set; } - /// - /// Date and time the cookie was created - /// - public DateTime TimeStamp { get; set; } - /// - /// Value of the cookie - /// - public string Value { get; set; } - /// - /// Version of the cookie - /// - public int Version { get; set; } - } -} - diff --git a/RestSharp/RestSharp.Compact.csproj b/RestSharp/RestSharp.Compact.csproj deleted file mode 100644 index 7dcba3d69..000000000 --- a/RestSharp/RestSharp.Compact.csproj +++ /dev/null @@ -1,160 +0,0 @@ - - - Debug - AnyCPU - 9.0.30729 - 2.0 - {A29E330B-F854-4287-BEB2-C4CBE9D9C637} - Library - Properties - RestSharp.Compact - RestSharp.Compact - {4D628B5B-2FBC-4AA6-8C16-197242AEB884};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - PocketPC - 4118C335-430C-497f-BE48-11C3316B135E - 5.1 - RestSharp.Compact - v3.5 - Windows Mobile 5.0 Pocket PC SDK - - - - - true - full - false - bin_compact\Debug\ - TRACE;DEBUG;PocketPC SIMPLE_JSON_NO_LINQ_EXPRESSION - true - true - prompt - 512 - 4 - Off - - - pdbonly - true - bin_compact\Release\ - TRACE;PocketPC SIMPLE_JSON_NO_LINQ_EXPRESSION FRAMEWORK - true - true - prompt - 512 - 4 - Off - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - T4Helper.log - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/RestSharp/RestSharp.csproj b/RestSharp/RestSharp.csproj deleted file mode 100644 index f5458aa24..000000000 --- a/RestSharp/RestSharp.csproj +++ /dev/null @@ -1,204 +0,0 @@ - - - - Debug - AnyCPU - 9.0.30729 - 2.0 - {2ECECFBF-5F3E-40EE-A963-72336DC7ABE2} - Library - Properties - RestSharp - RestSharp - v3.5 - 512 - - - 3.5 - - publish\ - true - Disk - false - Foreground - 7 - Days - false - false - true - 0 - 1.0.0.%2a - false - false - true - - - ..\ - true - - - true - full - false - bin\Debug\ - TRACE;DEBUG;FRAMEWORK - prompt - 4 - AllRules.ruleset - bin\Debug\RestSharp.xml - - - pdbonly - true - bin\Release\ - TRACE;FRAMEWORK - prompt - 4 - AllRules.ruleset - bin\Release\RestSharp.xml - 1591,1573,1658,1584,1574,1572 - true - - - - - - - 3.5 - - - 3.5 - - - 3.5 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - T4Helper.log - - - - - - - - - - - - - - - False - .NET Framework 3.5 SP1 Client Profile - false - - - False - .NET Framework 3.5 SP1 - true - - - False - Windows Installer 3.1 - true - - - - - - - - - - - - - - \ No newline at end of file diff --git a/RestSharp/Serializers/DotNetXmlSerializer.cs b/RestSharp/Serializers/DotNetXmlSerializer.cs deleted file mode 100644 index 8b3d2bed2..000000000 --- a/RestSharp/Serializers/DotNetXmlSerializer.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System.IO; -using System.Text; -using System.Xml.Serialization; - -namespace RestSharp.Serializers -{ - /// - /// Wrapper for System.Xml.Serialization.XmlSerializer. - /// - public class DotNetXmlSerializer : ISerializer - { - /// - /// Default constructor, does not specify namespace - /// - public DotNetXmlSerializer() - { - ContentType = "application/xml"; - Encoding = Encoding.UTF8; - } - - /// - /// Specify the namespaced to be used when serializing - /// - /// XML namespace - public DotNetXmlSerializer(string @namespace) : this() - { - Namespace = @namespace; - } - - /// - /// Serialize the object as XML - /// - /// Object to serialize - /// XML as string - public string Serialize(object obj) - { - var ns = new XmlSerializerNamespaces(); - ns.Add(string.Empty, Namespace); - var serializer = new System.Xml.Serialization.XmlSerializer(obj.GetType()); - var writer = new EncodingStringWriter(Encoding); - serializer.Serialize(writer, obj, ns); - - return writer.ToString(); - } - - /// - /// Name of the root element to use when serializing - /// - public string RootElement { get; set; } - - /// - /// XML namespace to use when serializing - /// - public string Namespace { get; set; } - - /// - /// Format string to use when serializing dates - /// - public string DateFormat { get; set; } - - /// - /// Content type for serialized content - /// - public string ContentType { get; set; } - - /// - /// Encoding for serialized content - /// - public Encoding Encoding { get; set; } - - /// - /// Need to subclass StringWriter in order to override Encoding - /// - private class EncodingStringWriter : StringWriter - { - private readonly Encoding encoding; - - public EncodingStringWriter(Encoding encoding) - { - this.encoding = encoding; - } - - public override Encoding Encoding - { - get { return encoding; } - } - } - } -} \ No newline at end of file diff --git a/RestSharp/Serializers/JsonSerializer.cs b/RestSharp/Serializers/JsonSerializer.cs deleted file mode 100644 index 191e7c4ca..000000000 --- a/RestSharp/Serializers/JsonSerializer.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.IO; - -namespace RestSharp.Serializers -{ - /// - /// Default JSON serializer for request bodies - /// Doesn't currently use the SerializeAs attribute, defers to Newtonsoft's attributes - /// - public class JsonSerializer : ISerializer - { - /// - /// Default serializer - /// - public JsonSerializer() - { - ContentType = "application/json"; - } - - /// - /// Serialize the object as JSON - /// - /// Object to serialize - /// JSON as String - public string Serialize(object obj) - { - return SimpleJson.SerializeObject(obj); - } - - /// - /// Unused for JSON Serialization - /// - public string DateFormat { get; set; } - /// - /// Unused for JSON Serialization - /// - public string RootElement { get; set; } - /// - /// Unused for JSON Serialization - /// - public string Namespace { get; set; } - /// - /// Content type for serialized content - /// - public string ContentType { get; set; } - } -} \ No newline at end of file diff --git a/RestSharp/Serializers/SerializeAsAttribute.cs b/RestSharp/Serializers/SerializeAsAttribute.cs deleted file mode 100644 index 98a89a74d..000000000 --- a/RestSharp/Serializers/SerializeAsAttribute.cs +++ /dev/null @@ -1,92 +0,0 @@ -#region License -// Copyright 2010 John Sheehan -// -// 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. -#endregion - -using System; -using RestSharp.Extensions; -using System.Globalization; - -namespace RestSharp.Serializers -{ - /// - /// Allows control how class and property names and values are serialized by XmlSerializer - /// Currently not supported with the JsonSerializer - /// When specified at the property level the class-level specification is overridden - /// - [AttributeUsage(AttributeTargets.Property | AttributeTargets.Class, Inherited = false, AllowMultiple = false)] - public sealed class SerializeAsAttribute : Attribute - { - public SerializeAsAttribute() { - NameStyle = NameStyle.AsIs; - Index = int.MaxValue; - Culture = CultureInfo.InvariantCulture; - } - - /// - /// The name to use for the serialized element - /// - public string Name { get; set; } - - /// - /// Sets the value to be serialized as an Attribute instead of an Element - /// - public bool Attribute { get; set; } - - /// - /// The culture to use when serializing - /// - public CultureInfo Culture { get; set; } - - /// - /// Transforms the casing of the name based on the selected value. - /// - public NameStyle NameStyle { get; set; } - - /// - /// The order to serialize the element. Default is int.MaxValue. - /// - public int Index { get; set; } - - /// - /// Called by the attribute when NameStyle is speficied - /// - /// The string to transform - /// String - public string TransformName(string input) { - var name = Name ?? input; - switch (NameStyle) { - case NameStyle.CamelCase: - return name.ToCamelCase(Culture); - case NameStyle.PascalCase: - return name.ToPascalCase(Culture); - case NameStyle.LowerCase: - return name.ToLower(); - } - - return input; - } - } - - /// - /// Options for transforming casing of element names - /// - public enum NameStyle - { - AsIs, - CamelCase, - LowerCase, - PascalCase - } -} diff --git a/RestSharp/Serializers/XmlSerializer.cs b/RestSharp/Serializers/XmlSerializer.cs deleted file mode 100644 index 35422b694..000000000 --- a/RestSharp/Serializers/XmlSerializer.cs +++ /dev/null @@ -1,245 +0,0 @@ -#region License -// Copyright 2010 John Sheehan -// -// 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. -#endregion - -using System; -using System.Collections; -using System.Globalization; -using System.Linq; -using System.Xml.Linq; -using RestSharp.Extensions; - -namespace RestSharp.Serializers -{ - /// - /// Default XML Serializer - /// - public class XmlSerializer : ISerializer - { - /// - /// Default constructor, does not specify namespace - /// - public XmlSerializer() { - ContentType = "text/xml"; - } - - /// - /// Specify the namespaced to be used when serializing - /// - /// XML namespace - public XmlSerializer(string @namespace) { - Namespace = @namespace; - ContentType = "text/xml"; - } - - /// - /// Serialize the object as XML - /// - /// Object to serialize - /// XML as string - public string Serialize(object obj) { - var doc = new XDocument(); - - var t = obj.GetType(); - var name = t.Name; - - var options = t.GetAttribute(); - if (options != null) { - name = options.TransformName(options.Name ?? name); - } - - var root = new XElement(name.AsNamespaced(Namespace)); - - if (obj is IList) - { - var itemTypeName = ""; - foreach (var item in (IList)obj) - { - var type = item.GetType(); - var opts = type.GetAttribute(); - if (opts != null) - { - itemTypeName = opts.TransformName(opts.Name ?? name); - } - if (itemTypeName == "") - { - itemTypeName = type.Name; - } - var instance = new XElement(itemTypeName); - Map(instance, item); - root.Add(instance); - } - } - else - Map(root, obj); - - if (RootElement.HasValue()) { - var wrapper = new XElement(RootElement.AsNamespaced(Namespace), root); - doc.Add(wrapper); - } - else { - doc.Add(root); - } - - return doc.ToString(); - } - - private void Map(XElement root, object obj) { - var objType = obj.GetType(); - - var props = from p in objType.GetProperties() - let indexAttribute = p.GetAttribute() - where p.CanRead && p.CanWrite - orderby indexAttribute == null ? int.MaxValue : indexAttribute.Index - select p; - - var globalOptions = objType.GetAttribute(); - - foreach (var prop in props) { - var name = prop.Name; - var rawValue = prop.GetValue(obj, null); - - if (rawValue == null) { - continue; - } - - var value = GetSerializedValue(rawValue); - var propType = prop.PropertyType; - - var useAttribute = false; - var settings = prop.GetAttribute(); - if (settings != null) { - name = settings.Name.HasValue() ? settings.Name : name; - useAttribute = settings.Attribute; - } - - var options = prop.GetAttribute(); - if (options != null) { - name = options.TransformName(name); - } - else if (globalOptions != null) { - name = globalOptions.TransformName(name); - } - - var nsName = name.AsNamespaced(Namespace); - var element = new XElement(nsName); - - if (propType.IsPrimitive || propType.IsValueType || propType == typeof(string)) { - if (useAttribute) { - root.Add(new XAttribute(name, value)); - continue; - } - - element.Value = value; - } - else if (rawValue is IList) { - var itemTypeName = ""; - foreach (var item in (IList)rawValue) { - if (itemTypeName == "") - { - var type = item.GetType(); - var setting = type.GetAttribute(); - itemTypeName = setting != null && setting.Name.HasValue() - ? setting.Name - : type.Name; - } - var instance = new XElement(itemTypeName); - Map(instance, item); - element.Add(instance); - } - } - else { - Map(element, rawValue); - } - - root.Add(element); - } - } - - private string GetSerializedValue(object obj) { - var output = obj; - - if (obj is DateTime && DateFormat.HasValue()) - { - output = ((DateTime) obj).ToString(DateFormat, CultureInfo.InvariantCulture); - } - if (obj is bool) - { - output = ((bool)obj).ToString(CultureInfo.InvariantCulture).ToLower(); - } - if (IsNumeric(obj)) - { - return SerializeNumber(obj); - } - - return output.ToString(); - } - - static string SerializeNumber(object number) - { - if (number is long) - return ((long)number).ToString(CultureInfo.InvariantCulture); - else if (number is ulong) - return ((ulong)number).ToString(CultureInfo.InvariantCulture); - else if (number is int) - return ((int)number).ToString(CultureInfo.InvariantCulture); - else if (number is uint) - return ((uint)number).ToString(CultureInfo.InvariantCulture); - else if (number is decimal) - return ((decimal)number).ToString(CultureInfo.InvariantCulture); - else if (number is float) - return ((float)number).ToString(CultureInfo.InvariantCulture); - else - return (Convert.ToDouble(number, CultureInfo.InvariantCulture).ToString("r", CultureInfo.InvariantCulture)); - } - - /// - /// Determines if a given object is numeric in any way - /// (can be integer, double, null, etc). - /// - static bool IsNumeric(object value) - { - if (value is sbyte) return true; - if (value is byte) return true; - if (value is short) return true; - if (value is ushort) return true; - if (value is int) return true; - if (value is uint) return true; - if (value is long) return true; - if (value is ulong) return true; - if (value is float) return true; - if (value is double) return true; - if (value is decimal) return true; - return false; - } - - /// - /// Name of the root element to use when serializing - /// - public string RootElement { get; set; } - /// - /// XML namespace to use when serializing - /// - public string Namespace { get; set; } - /// - /// Format string to use when serializing dates - /// - public string DateFormat { get; set; } - /// - /// Content type for serialized content - /// - public string ContentType { get; set; } - } -} \ No newline at end of file diff --git a/RestSharp/SharedAssemblyInfo.cs b/RestSharp/SharedAssemblyInfo.cs deleted file mode 100644 index c7f7ed61b..000000000 --- a/RestSharp/SharedAssemblyInfo.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyDescription("Simple REST and HTTP API Client")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("John Sheehan, RestSharp Community")] -[assembly: AssemblyProduct("RestSharp")] -[assembly: AssemblyCopyright("Copyright © RestSharp Project 2009-2012")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] -[assembly: CLSCompliant(true)] -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion(SharedAssembylInfo.Version + ".0")] -[assembly: AssemblyInformationalVersion(SharedAssembylInfo.Version)] - -#if !PocketPC -[assembly: AssemblyFileVersion(SharedAssembylInfo.Version + ".0")] -#endif - -class SharedAssembylInfo -{ - public const string Version = "104.4.0"; -} diff --git a/RestSharp/SimpleJson.cs b/RestSharp/SimpleJson.cs deleted file mode 100644 index e5fc1066f..000000000 --- a/RestSharp/SimpleJson.cs +++ /dev/null @@ -1,2094 +0,0 @@ -//----------------------------------------------------------------------- -// -// Copyright (c) 2011, The Outercurve Foundation. -// -// Licensed under the MIT License (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.opensource.org/licenses/mit-license.php -// -// 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. -// -// Nathan Totten (ntotten.com), Jim Zimmerman (jimzimmerman.com) and Prabir Shrestha (prabir.me) -// https://github.com/facebook-csharp-sdk/simple-json -//----------------------------------------------------------------------- - -// VERSION: 0.26.0 - -// NOTE: uncomment the following line to make SimpleJson class internal. -//#define SIMPLE_JSON_INTERNAL - -// NOTE: uncomment the following line to make JsonArray and JsonObject class internal. -//#define SIMPLE_JSON_OBJARRAYINTERNAL - -// NOTE: uncomment the following line to enable dynamic support. -//#define SIMPLE_JSON_DYNAMIC - -// NOTE: uncomment the following line to enable DataContract support. -//#define SIMPLE_JSON_DATACONTRACT - -// NOTE: uncomment the following line to disable linq expressions/compiled lambda (better performance) instead of method.invoke(). -// define if you are using .net framework <= 3.0 or < WP7.5 -//#define SIMPLE_JSON_NO_LINQ_EXPRESSION - -// NOTE: uncomment the following line if you are compiling under Window Metro style application/library. -// usually already defined in properties -//#define NETFX_CORE; - -// If you are targetting WinStore, WP8 and NET4.5+ PCL make sure to #define SIMPLE_JSON_TYPEINFO; - -// original json parsing code from http://techblog.procurios.nl/k/618/news/view/14605/14863/How-do-I-write-my-own-parser-for-JSON.html - -#if NETFX_CORE -#define SIMPLE_JSON_TYPEINFO -#endif - -using System; -using System.CodeDom.Compiler; -using System.Collections; -using System.Collections.Generic; -#if !SIMPLE_JSON_NO_LINQ_EXPRESSION -using System.Linq.Expressions; -#endif -using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; -#if SIMPLE_JSON_DYNAMIC -using System.Dynamic; -#endif -using System.Globalization; -using System.Reflection; -using System.Runtime.Serialization; -using System.Text; -using RestSharp.Reflection; - -// ReSharper disable LoopCanBeConvertedToQuery -// ReSharper disable RedundantExplicitArrayCreation -// ReSharper disable SuggestUseVarKeywordEvident -namespace RestSharp -{ - /// - /// Represents the json array. - /// - [GeneratedCode("simple-json", "1.0.0")] - [EditorBrowsable(EditorBrowsableState.Never)] - [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix")] -#if SIMPLE_JSON_OBJARRAYINTERNAL - internal -#else - public -#endif - class JsonArray : List - { - /// - /// Initializes a new instance of the class. - /// - public JsonArray() { } - - /// - /// Initializes a new instance of the class. - /// - /// The capacity of the json array. - public JsonArray(int capacity) : base(capacity) { } - - /// - /// The json representation of the array. - /// - /// The json representation of the array. - public override string ToString() - { - return SimpleJson.SerializeObject(this) ?? string.Empty; - } - } - - /// - /// Represents the json object. - /// - [GeneratedCode("simple-json", "1.0.0")] - [EditorBrowsable(EditorBrowsableState.Never)] - [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix")] -#if SIMPLE_JSON_OBJARRAYINTERNAL - internal -#else - public -#endif - class JsonObject : -#if SIMPLE_JSON_DYNAMIC - DynamicObject, -#endif - IDictionary - { - /// - /// The internal member dictionary. - /// - private readonly Dictionary _members; - - /// - /// Initializes a new instance of . - /// - public JsonObject() - { - _members = new Dictionary(); - } - - /// - /// Initializes a new instance of . - /// - /// The implementation to use when comparing keys, or null to use the default for the type of the key. - public JsonObject(IEqualityComparer comparer) - { - _members = new Dictionary(comparer); - } - - /// - /// Gets the at the specified index. - /// - /// - public object this[int index] - { - get { return GetAtIndex(_members, index); } - } - - internal static object GetAtIndex(IDictionary obj, int index) - { - if (obj == null) - throw new ArgumentNullException("obj"); - if (index >= obj.Count) - throw new ArgumentOutOfRangeException("index"); - int i = 0; - foreach (KeyValuePair o in obj) - if (i++ == index) return o.Value; - return null; - } - - /// - /// Adds the specified key. - /// - /// The key. - /// The value. - public void Add(string key, object value) - { - _members.Add(key, value); - } - - /// - /// Determines whether the specified key contains key. - /// - /// The key. - /// - /// true if the specified key contains key; otherwise, false. - /// - public bool ContainsKey(string key) - { - return _members.ContainsKey(key); - } - - /// - /// Gets the keys. - /// - /// The keys. - public ICollection Keys - { - get { return _members.Keys; } - } - - /// - /// Removes the specified key. - /// - /// The key. - /// - public bool Remove(string key) - { - return _members.Remove(key); - } - - /// - /// Tries the get value. - /// - /// The key. - /// The value. - /// - public bool TryGetValue(string key, out object value) - { - return _members.TryGetValue(key, out value); - } - - /// - /// Gets the values. - /// - /// The values. - public ICollection Values - { - get { return _members.Values; } - } - - /// - /// Gets or sets the with the specified key. - /// - /// - public object this[string key] - { - get { return _members[key]; } - set { _members[key] = value; } - } - - /// - /// Adds the specified item. - /// - /// The item. - public void Add(KeyValuePair item) - { - _members.Add(item.Key, item.Value); - } - - /// - /// Clears this instance. - /// - public void Clear() - { - _members.Clear(); - } - - /// - /// Determines whether [contains] [the specified item]. - /// - /// The item. - /// - /// true if [contains] [the specified item]; otherwise, false. - /// - public bool Contains(KeyValuePair item) - { - return _members.ContainsKey(item.Key) && _members[item.Key] == item.Value; - } - - /// - /// Copies to. - /// - /// The array. - /// Index of the array. - public void CopyTo(KeyValuePair[] array, int arrayIndex) - { - if (array == null) throw new ArgumentNullException("array"); - int num = Count; - foreach (KeyValuePair kvp in this) - { - array[arrayIndex++] = kvp; - if (--num <= 0) - return; - } - } - - /// - /// Gets the count. - /// - /// The count. - public int Count - { - get { return _members.Count; } - } - - /// - /// Gets a value indicating whether this instance is read only. - /// - /// - /// true if this instance is read only; otherwise, false. - /// - public bool IsReadOnly - { - get { return false; } - } - - /// - /// Removes the specified item. - /// - /// The item. - /// - public bool Remove(KeyValuePair item) - { - return _members.Remove(item.Key); - } - - /// - /// Gets the enumerator. - /// - /// - public IEnumerator> GetEnumerator() - { - return _members.GetEnumerator(); - } - - /// - /// Returns an enumerator that iterates through a collection. - /// - /// - /// An object that can be used to iterate through the collection. - /// - IEnumerator IEnumerable.GetEnumerator() - { - return _members.GetEnumerator(); - } - - /// - /// Returns a json that represents the current . - /// - /// - /// A json that represents the current . - /// - public override string ToString() - { - return SimpleJson.SerializeObject(this); - } - -#if SIMPLE_JSON_DYNAMIC - /// - /// Provides implementation for type conversion operations. Classes derived from the class can override this method to specify dynamic behavior for operations that convert an object from one type to another. - /// - /// Provides information about the conversion operation. The binder.Type property provides the type to which the object must be converted. For example, for the statement (String)sampleObject in C# (CType(sampleObject, Type) in Visual Basic), where sampleObject is an instance of the class derived from the class, binder.Type returns the type. The binder.Explicit property provides information about the kind of conversion that occurs. It returns true for explicit conversion and false for implicit conversion. - /// The result of the type conversion operation. - /// - /// Alwasy returns true. - /// - public override bool TryConvert(ConvertBinder binder, out object result) - { - // - if (binder == null) - throw new ArgumentNullException("binder"); - // - Type targetType = binder.Type; - - if ((targetType == typeof(IEnumerable)) || - (targetType == typeof(IEnumerable>)) || - (targetType == typeof(IDictionary)) || - (targetType == typeof(IDictionary))) - { - result = this; - return true; - } - - return base.TryConvert(binder, out result); - } - - /// - /// Provides the implementation for operations that delete an object member. This method is not intended for use in C# or Visual Basic. - /// - /// Provides information about the deletion. - /// - /// Alwasy returns true. - /// - public override bool TryDeleteMember(DeleteMemberBinder binder) - { - // - if (binder == null) - throw new ArgumentNullException("binder"); - // - return _members.Remove(binder.Name); - } - - /// - /// Provides the implementation for operations that get a value by index. Classes derived from the class can override this method to specify dynamic behavior for indexing operations. - /// - /// Provides information about the operation. - /// The indexes that are used in the operation. For example, for the sampleObject[3] operation in C# (sampleObject(3) in Visual Basic), where sampleObject is derived from the DynamicObject class, is equal to 3. - /// The result of the index operation. - /// - /// Alwasy returns true. - /// - public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result) - { - if (indexes == null) throw new ArgumentNullException("indexes"); - if (indexes.Length == 1) - { - result = ((IDictionary)this)[(string)indexes[0]]; - return true; - } - result = null; - return true; - } - - /// - /// Provides the implementation for operations that get member values. Classes derived from the class can override this method to specify dynamic behavior for operations such as getting a value for a property. - /// - /// Provides information about the object that called the dynamic operation. The binder.Name property provides the name of the member on which the dynamic operation is performed. For example, for the Console.WriteLine(sampleObject.SampleProperty) statement, where sampleObject is an instance of the class derived from the class, binder.Name returns "SampleProperty". The binder.IgnoreCase property specifies whether the member name is case-sensitive. - /// The result of the get operation. For example, if the method is called for a property, you can assign the property value to . - /// - /// Alwasy returns true. - /// - public override bool TryGetMember(GetMemberBinder binder, out object result) - { - object value; - if (_members.TryGetValue(binder.Name, out value)) - { - result = value; - return true; - } - result = null; - return true; - } - - /// - /// Provides the implementation for operations that set a value by index. Classes derived from the class can override this method to specify dynamic behavior for operations that access objects by a specified index. - /// - /// Provides information about the operation. - /// The indexes that are used in the operation. For example, for the sampleObject[3] = 10 operation in C# (sampleObject(3) = 10 in Visual Basic), where sampleObject is derived from the class, is equal to 3. - /// The value to set to the object that has the specified index. For example, for the sampleObject[3] = 10 operation in C# (sampleObject(3) = 10 in Visual Basic), where sampleObject is derived from the class, is equal to 10. - /// - /// true if the operation is successful; otherwise, false. If this method returns false, the run-time binder of the language determines the behavior. (In most cases, a language-specific run-time exception is thrown. - /// - public override bool TrySetIndex(SetIndexBinder binder, object[] indexes, object value) - { - if (indexes == null) throw new ArgumentNullException("indexes"); - if (indexes.Length == 1) - { - ((IDictionary)this)[(string)indexes[0]] = value; - return true; - } - return base.TrySetIndex(binder, indexes, value); - } - - /// - /// Provides the implementation for operations that set member values. Classes derived from the class can override this method to specify dynamic behavior for operations such as setting a value for a property. - /// - /// Provides information about the object that called the dynamic operation. The binder.Name property provides the name of the member to which the value is being assigned. For example, for the statement sampleObject.SampleProperty = "Test", where sampleObject is an instance of the class derived from the class, binder.Name returns "SampleProperty". The binder.IgnoreCase property specifies whether the member name is case-sensitive. - /// The value to set to the member. For example, for sampleObject.SampleProperty = "Test", where sampleObject is an instance of the class derived from the class, the is "Test". - /// - /// true if the operation is successful; otherwise, false. If this method returns false, the run-time binder of the language determines the behavior. (In most cases, a language-specific run-time exception is thrown.) - /// - public override bool TrySetMember(SetMemberBinder binder, object value) - { - // - if (binder == null) - throw new ArgumentNullException("binder"); - // - _members[binder.Name] = value; - return true; - } - - /// - /// Returns the enumeration of all dynamic member names. - /// - /// - /// A sequence that contains dynamic member names. - /// - public override IEnumerable GetDynamicMemberNames() - { - foreach (var key in Keys) - yield return key; - } -#endif - } -} - -namespace RestSharp -{ - /// - /// This class encodes and decodes JSON strings. - /// Spec. details, see http://www.json.org/ - /// - /// JSON uses Arrays and Objects. These correspond here to the datatypes JsonArray(IList<object>) and JsonObject(IDictionary<string,object>). - /// All numbers are parsed to doubles. - /// - [GeneratedCode("simple-json", "1.0.0")] -#if SIMPLE_JSON_INTERNAL - internal -#else - public -#endif - static class SimpleJson - { - private const int TOKEN_NONE = 0; - private const int TOKEN_CURLY_OPEN = 1; - private const int TOKEN_CURLY_CLOSE = 2; - private const int TOKEN_SQUARED_OPEN = 3; - private const int TOKEN_SQUARED_CLOSE = 4; - private const int TOKEN_COLON = 5; - private const int TOKEN_COMMA = 6; - private const int TOKEN_STRING = 7; - private const int TOKEN_NUMBER = 8; - private const int TOKEN_TRUE = 9; - private const int TOKEN_FALSE = 10; - private const int TOKEN_NULL = 11; - private const int BUILDER_CAPACITY = 2000; - - /// - /// Parses the string json into a value - /// - /// A JSON string. - /// An IList<object>, a IDictionary<string,object>, a double, a string, null, true, or false - public static object DeserializeObject(string json) - { - object obj; - if (TryDeserializeObject(json, out obj)) - return obj; - throw new SerializationException("Invalid JSON string"); - } - - /// - /// Try parsing the json string into a value. - /// - /// - /// A JSON string. - /// - /// - /// The object. - /// - /// - /// Returns true if successfull otherwise false. - /// - [SuppressMessage("Microsoft.Design", "CA1007:UseGenericsWhereAppropriate", Justification="Need to support .NET 2")] - public static bool TryDeserializeObject(string json, out object obj) - { - bool success = true; - if (json != null) - { - char[] charArray = json.ToCharArray(); - int index = 0; - obj = ParseValue(charArray, ref index, ref success); - } - else - obj = null; - - return success; - } - - public static object DeserializeObject(string json, Type type, IJsonSerializerStrategy jsonSerializerStrategy) - { - object jsonObject = DeserializeObject(json); - return type == null || jsonObject != null && ReflectionUtils.IsAssignableFrom(jsonObject.GetType(), type) - ? jsonObject - : (jsonSerializerStrategy ?? CurrentJsonSerializerStrategy).DeserializeObject(jsonObject, type); - } - - public static object DeserializeObject(string json, Type type) - { - return DeserializeObject(json, type, null); - } - - public static T DeserializeObject(string json, IJsonSerializerStrategy jsonSerializerStrategy) - { - return (T)DeserializeObject(json, typeof(T), jsonSerializerStrategy); - } - - public static T DeserializeObject(string json) - { - return (T)DeserializeObject(json, typeof(T), null); - } - - /// - /// Converts a IDictionary<string,object> / IList<object> object into a JSON string - /// - /// A IDictionary<string,object> / IList<object> - /// Serializer strategy to use - /// A JSON encoded string, or null if object 'json' is not serializable - public static string SerializeObject(object json, IJsonSerializerStrategy jsonSerializerStrategy) - { - StringBuilder builder = new StringBuilder(BUILDER_CAPACITY); - bool success = SerializeValue(jsonSerializerStrategy, json, builder); - return (success ? builder.ToString() : null); - } - - public static string SerializeObject(object json) - { - return SerializeObject(json, CurrentJsonSerializerStrategy); - } - - public static string EscapeToJavascriptString(string jsonString) - { - if (string.IsNullOrEmpty(jsonString)) - return jsonString; - - StringBuilder sb = new StringBuilder(); - char c; - - for (int i = 0; i < jsonString.Length; ) - { - c = jsonString[i++]; - - if (c == '\\') - { - int remainingLength = jsonString.Length - i; - if (remainingLength >= 2) - { - char lookahead = jsonString[i]; - if (lookahead == '\\') - { - sb.Append('\\'); - ++i; - } - else if (lookahead == '"') - { - sb.Append("\""); - ++i; - } - else if (lookahead == 't') - { - sb.Append('\t'); - ++i; - } - else if (lookahead == 'b') - { - sb.Append('\b'); - ++i; - } - else if (lookahead == 'n') - { - sb.Append('\n'); - ++i; - } - else if (lookahead == 'r') - { - sb.Append('\r'); - ++i; - } - } - } - else - { - sb.Append(c); - } - } - return sb.ToString(); - } - - static IDictionary ParseObject(char[] json, ref int index, ref bool success) - { - IDictionary table = new JsonObject(); - int token; - - // { - NextToken(json, ref index); - - bool done = false; - while (!done) - { - token = LookAhead(json, index); - if (token == TOKEN_NONE) - { - success = false; - return null; - } - else if (token == TOKEN_COMMA) - NextToken(json, ref index); - else if (token == TOKEN_CURLY_CLOSE) - { - NextToken(json, ref index); - return table; - } - else - { - // name - string name = ParseString(json, ref index, ref success); - if (!success) - { - success = false; - return null; - } - // : - token = NextToken(json, ref index); - if (token != TOKEN_COLON) - { - success = false; - return null; - } - // value - object value = ParseValue(json, ref index, ref success); - if (!success) - { - success = false; - return null; - } - table[name] = value; - } - } - return table; - } - - static JsonArray ParseArray(char[] json, ref int index, ref bool success) - { - JsonArray array = new JsonArray(); - - // [ - NextToken(json, ref index); - - bool done = false; - while (!done) - { - int token = LookAhead(json, index); - if (token == TOKEN_NONE) - { - success = false; - return null; - } - else if (token == TOKEN_COMMA) - NextToken(json, ref index); - else if (token == TOKEN_SQUARED_CLOSE) - { - NextToken(json, ref index); - break; - } - else - { - object value = ParseValue(json, ref index, ref success); - if (!success) - return null; - array.Add(value); - } - } - return array; - } - - static object ParseValue(char[] json, ref int index, ref bool success) - { - switch (LookAhead(json, index)) - { - case TOKEN_STRING: - return ParseString(json, ref index, ref success); - case TOKEN_NUMBER: - return ParseNumber(json, ref index, ref success); - case TOKEN_CURLY_OPEN: - return ParseObject(json, ref index, ref success); - case TOKEN_SQUARED_OPEN: - return ParseArray(json, ref index, ref success); - case TOKEN_TRUE: - NextToken(json, ref index); - return true; - case TOKEN_FALSE: - NextToken(json, ref index); - return false; - case TOKEN_NULL: - NextToken(json, ref index); - return null; - case TOKEN_NONE: - break; - } - success = false; - return null; - } - - static string ParseString(char[] json, ref int index, ref bool success) - { - StringBuilder s = new StringBuilder(BUILDER_CAPACITY); - char c; - - EatWhitespace(json, ref index); - - // " - c = json[index++]; - bool complete = false; - while (!complete) - { - if (index == json.Length) - break; - - c = json[index++]; - if (c == '"') - { - complete = true; - break; - } - else if (c == '\\') - { - if (index == json.Length) - break; - c = json[index++]; - if (c == '"') - s.Append('"'); - else if (c == '\\') - s.Append('\\'); - else if (c == '/') - s.Append('/'); - else if (c == 'b') - s.Append('\b'); - else if (c == 'f') - s.Append('\f'); - else if (c == 'n') - s.Append('\n'); - else if (c == 'r') - s.Append('\r'); - else if (c == 't') - s.Append('\t'); - else if (c == 'u') - { - int remainingLength = json.Length - index; - if (remainingLength >= 4) - { - // parse the 32 bit hex into an integer codepoint - uint codePoint; - -#if PocketPC - try { - codePoint = UInt32.Parse(new string(json, index, 4), NumberStyles.HexNumber); - } catch (Exception ex) { - return ""; - } -#else - if (!(success = UInt32.TryParse(new string(json, index, 4), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out codePoint))) - return ""; -#endif - // convert the integer codepoint to a unicode char and add to string - if (0xD800 <= codePoint && codePoint <= 0xDBFF) // if high surrogate - { - index += 4; // skip 4 chars - remainingLength = json.Length - index; - if (remainingLength >= 6) - { - uint lowCodePoint = 0; - -#if PocketPC - bool lowCodePointPassed = false; - try { - lowCodePoint = UInt32.Parse(new string(json, index + 2, 4), NumberStyles.HexNumber); - lowCodePointPassed = true; - } catch (Exception) { } - if (new string(json, index, 2) == "\\u" && lowCodePointPassed) -#else - if (new string(json, index, 2) == "\\u" && UInt32.TryParse(new string(json, index + 2, 4), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out lowCodePoint)) -#endif - { - if (0xDC00 <= lowCodePoint && lowCodePoint <= 0xDFFF) // if low surrogate - { - s.Append((char)codePoint); - s.Append((char)lowCodePoint); - index += 6; // skip 6 chars - continue; - } - } - } - success = false; // invalid surrogate pair - return ""; - } - s.Append(ConvertFromUtf32((int)codePoint)); - // skip 4 chars - index += 4; - } - else - break; - } - } - else - s.Append(c); - } - if (!complete) - { - success = false; - return null; - } - return s.ToString(); - } - - private static string ConvertFromUtf32(int utf32) - { - // http://www.java2s.com/Open-Source/CSharp/2.6.4-mono-.net-core/System/System/Char.cs.htm - if (utf32 < 0 || utf32 > 0x10FFFF) - throw new ArgumentOutOfRangeException("utf32", "The argument must be from 0 to 0x10FFFF."); - if (0xD800 <= utf32 && utf32 <= 0xDFFF) - throw new ArgumentOutOfRangeException("utf32", "The argument must not be in surrogate pair range."); - if (utf32 < 0x10000) - return new string((char)utf32, 1); - utf32 -= 0x10000; - return new string(new char[] { (char)((utf32 >> 10) + 0xD800), (char)(utf32 % 0x0400 + 0xDC00) }); - } - - static object ParseNumber(char[] json, ref int index, ref bool success) - { - EatWhitespace(json, ref index); - int lastIndex = GetLastIndexOfNumber(json, index); - int charLength = (lastIndex - index) + 1; - object returnNumber; - string str = new string(json, index, charLength); - if (str.IndexOf(".", StringComparison.OrdinalIgnoreCase) != -1 || str.IndexOf("e", StringComparison.OrdinalIgnoreCase) != -1) - { - double number; -#if PocketPC - try { - number = double.Parse(new string(json, index, charLength), NumberStyles.Any); - success = true; - } catch (Exception) { - number = 0; - success = false; - } -#else - success = double.TryParse(new string(json, index, charLength), NumberStyles.Any, CultureInfo.InvariantCulture, out number); -#endif - returnNumber = number; - } - else - { - long number; -#if PocketPC - try { - number = long.Parse(new string(json, index, charLength), NumberStyles.Any); - success = true; - } catch (Exception) { - number = 0; - success = false; - } -#else - success = long.TryParse(new string(json, index, charLength), NumberStyles.Any, CultureInfo.InvariantCulture, out number); -#endif - returnNumber = number; - } - index = lastIndex + 1; - return returnNumber; - } - - static int GetLastIndexOfNumber(char[] json, int index) - { - int lastIndex; - for (lastIndex = index; lastIndex < json.Length; lastIndex++) - if ("0123456789+-.eE".IndexOf(json[lastIndex]) == -1) break; - return lastIndex - 1; - } - - static void EatWhitespace(char[] json, ref int index) - { - for (; index < json.Length; index++) - if (" \t\n\r\b\f".IndexOf(json[index]) == -1) break; - } - - static int LookAhead(char[] json, int index) - { - int saveIndex = index; - return NextToken(json, ref saveIndex); - } - - [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")] - static int NextToken(char[] json, ref int index) - { - EatWhitespace(json, ref index); - if (index == json.Length) - return TOKEN_NONE; - char c = json[index]; - index++; - switch (c) - { - case '{': - return TOKEN_CURLY_OPEN; - case '}': - return TOKEN_CURLY_CLOSE; - case '[': - return TOKEN_SQUARED_OPEN; - case ']': - return TOKEN_SQUARED_CLOSE; - case ',': - return TOKEN_COMMA; - case '"': - return TOKEN_STRING; - case '0': - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': - case '8': - case '9': - case '-': - return TOKEN_NUMBER; - case ':': - return TOKEN_COLON; - } - index--; - int remainingLength = json.Length - index; - // false - if (remainingLength >= 5) - { - if (json[index] == 'f' && json[index + 1] == 'a' && json[index + 2] == 'l' && json[index + 3] == 's' && json[index + 4] == 'e') - { - index += 5; - return TOKEN_FALSE; - } - } - // true - if (remainingLength >= 4) - { - if (json[index] == 't' && json[index + 1] == 'r' && json[index + 2] == 'u' && json[index + 3] == 'e') - { - index += 4; - return TOKEN_TRUE; - } - } - // null - if (remainingLength >= 4) - { - if (json[index] == 'n' && json[index + 1] == 'u' && json[index + 2] == 'l' && json[index + 3] == 'l') - { - index += 4; - return TOKEN_NULL; - } - } - return TOKEN_NONE; - } - - static bool SerializeValue(IJsonSerializerStrategy jsonSerializerStrategy, object value, StringBuilder builder) - { - bool success = true; - string stringValue = value as string; - if (stringValue != null) - success = SerializeString(stringValue, builder); - else - { - IDictionary dict = value as IDictionary; - if (dict != null) - { - success = SerializeObject(jsonSerializerStrategy, dict.Keys, dict.Values, builder); - } - else - { - IDictionary stringDictionary = value as IDictionary; - if (stringDictionary != null) - { - success = SerializeObject(jsonSerializerStrategy, stringDictionary.Keys, stringDictionary.Values, builder); - } - else - { - IEnumerable enumerableValue = value as IEnumerable; - if (enumerableValue != null) - success = SerializeArray(jsonSerializerStrategy, enumerableValue, builder); - else if (IsNumeric(value)) - success = SerializeNumber(value, builder); - else if (value is bool) - builder.Append((bool)value ? "true" : "false"); - else if (value == null) - builder.Append("null"); - else - { - object serializedObject; - success = jsonSerializerStrategy.TrySerializeNonPrimitiveObject(value, out serializedObject); - if (success) - SerializeValue(jsonSerializerStrategy, serializedObject, builder); - } - } - } - } - return success; - } - - static bool SerializeObject(IJsonSerializerStrategy jsonSerializerStrategy, IEnumerable keys, IEnumerable values, StringBuilder builder) - { - builder.Append("{"); - IEnumerator ke = keys.GetEnumerator(); - IEnumerator ve = values.GetEnumerator(); - bool first = true; - while (ke.MoveNext() && ve.MoveNext()) - { - object key = ke.Current; - object value = ve.Current; - if (!first) - builder.Append(","); - string stringKey = key as string; - if (stringKey != null) - SerializeString(stringKey, builder); - else - if (!SerializeValue(jsonSerializerStrategy, value, builder)) return false; - builder.Append(":"); - if (!SerializeValue(jsonSerializerStrategy, value, builder)) - return false; - first = false; - } - builder.Append("}"); - return true; - } - - static bool SerializeArray(IJsonSerializerStrategy jsonSerializerStrategy, IEnumerable anArray, StringBuilder builder) - { - builder.Append("["); - bool first = true; - foreach (object value in anArray) - { - if (!first) - builder.Append(","); - if (!SerializeValue(jsonSerializerStrategy, value, builder)) - return false; - first = false; - } - builder.Append("]"); - return true; - } - - static bool SerializeString(string aString, StringBuilder builder) - { - builder.Append("\""); - char[] charArray = aString.ToCharArray(); - for (int i = 0; i < charArray.Length; i++) - { - char c = charArray[i]; - if (c == '"') - builder.Append("\\\""); - else if (c == '\\') - builder.Append("\\\\"); - else if (c == '\b') - builder.Append("\\b"); - else if (c == '\f') - builder.Append("\\f"); - else if (c == '\n') - builder.Append("\\n"); - else if (c == '\r') - builder.Append("\\r"); - else if (c == '\t') - builder.Append("\\t"); - else - builder.Append(c); - } - builder.Append("\""); - return true; - } - - static bool SerializeNumber(object number, StringBuilder builder) - { - if (number is long) - builder.Append(((long)number).ToString(CultureInfo.InvariantCulture)); - else if (number is ulong) - builder.Append(((ulong)number).ToString(CultureInfo.InvariantCulture)); - else if (number is int) - builder.Append(((int)number).ToString(CultureInfo.InvariantCulture)); - else if (number is uint) - builder.Append(((uint)number).ToString(CultureInfo.InvariantCulture)); - else if (number is decimal) - builder.Append(((decimal)number).ToString(CultureInfo.InvariantCulture)); - else if (number is float) - builder.Append(((float)number).ToString(CultureInfo.InvariantCulture)); - else - builder.Append(Convert.ToDouble(number, CultureInfo.InvariantCulture).ToString("r", CultureInfo.InvariantCulture)); - return true; - } - - /// - /// Determines if a given object is numeric in any way - /// (can be integer, double, null, etc). - /// - static bool IsNumeric(object value) - { - if (value is sbyte) return true; - if (value is byte) return true; - if (value is short) return true; - if (value is ushort) return true; - if (value is int) return true; - if (value is uint) return true; - if (value is long) return true; - if (value is ulong) return true; - if (value is float) return true; - if (value is double) return true; - if (value is decimal) return true; - return false; - } - - private static IJsonSerializerStrategy _currentJsonSerializerStrategy; - public static IJsonSerializerStrategy CurrentJsonSerializerStrategy - { - get - { - return _currentJsonSerializerStrategy ?? - (_currentJsonSerializerStrategy = -#if SIMPLE_JSON_DATACONTRACT - DataContractJsonSerializerStrategy -#else - PocoJsonSerializerStrategy -#endif -); - } - set - { - _currentJsonSerializerStrategy = value; - } - } - - private static PocoJsonSerializerStrategy _pocoJsonSerializerStrategy; - [EditorBrowsable(EditorBrowsableState.Advanced)] - public static PocoJsonSerializerStrategy PocoJsonSerializerStrategy - { - get - { - return _pocoJsonSerializerStrategy ?? (_pocoJsonSerializerStrategy = new PocoJsonSerializerStrategy()); - } - } - -#if SIMPLE_JSON_DATACONTRACT - - private static DataContractJsonSerializerStrategy _dataContractJsonSerializerStrategy; - [System.ComponentModel.EditorBrowsable(EditorBrowsableState.Advanced)] - public static DataContractJsonSerializerStrategy DataContractJsonSerializerStrategy - { - get - { - return _dataContractJsonSerializerStrategy ?? (_dataContractJsonSerializerStrategy = new DataContractJsonSerializerStrategy()); - } - } - -#endif - } - - [GeneratedCode("simple-json", "1.0.0")] -#if SIMPLE_JSON_INTERNAL - internal -#else - public -#endif - interface IJsonSerializerStrategy - { - [SuppressMessage("Microsoft.Design", "CA1007:UseGenericsWhereAppropriate", Justification="Need to support .NET 2")] - bool TrySerializeNonPrimitiveObject(object input, out object output); - object DeserializeObject(object value, Type type); - } - - [GeneratedCode("simple-json", "1.0.0")] -#if SIMPLE_JSON_INTERNAL - internal -#else - public -#endif - class PocoJsonSerializerStrategy : IJsonSerializerStrategy - { - internal IDictionary ConstructorCache; - internal IDictionary> GetCache; - internal IDictionary>> SetCache; - - internal static readonly Type[] EmptyTypes = new Type[0]; - internal static readonly Type[] ArrayConstructorParameterTypes = new Type[] { typeof(int) }; - - private static readonly string[] Iso8601Format = new string[] - { - @"yyyy-MM-dd\THH:mm:ss.FFFFFFF\Z", - @"yyyy-MM-dd\THH:mm:ss\Z", - @"yyyy-MM-dd\THH:mm:ssK" - }; - - public PocoJsonSerializerStrategy() - { - ConstructorCache = new ReflectionUtils.ThreadSafeDictionary(ContructorDelegateFactory); - GetCache = new ReflectionUtils.ThreadSafeDictionary>(GetterValueFactory); - SetCache = new ReflectionUtils.ThreadSafeDictionary>>(SetterValueFactory); - } - - protected virtual string MapClrMemberNameToJsonFieldName(string clrPropertyName) - { - return clrPropertyName; - } - - internal virtual ReflectionUtils.ConstructorDelegate ContructorDelegateFactory(Type key) - { - return ReflectionUtils.GetContructor(key, key.IsArray ? ArrayConstructorParameterTypes : EmptyTypes); - } - - internal virtual IDictionary GetterValueFactory(Type type) - { - IDictionary result = new Dictionary(); - foreach (PropertyInfo propertyInfo in ReflectionUtils.GetProperties(type)) - { - if (propertyInfo.CanRead) - { - MethodInfo getMethod = ReflectionUtils.GetGetterMethodInfo(propertyInfo); - if (getMethod.IsStatic || !getMethod.IsPublic) - continue; - result[MapClrMemberNameToJsonFieldName(propertyInfo.Name)] = ReflectionUtils.GetGetMethod(propertyInfo); - } - } - foreach (FieldInfo fieldInfo in ReflectionUtils.GetFields(type)) - { - if (fieldInfo.IsStatic || !fieldInfo.IsPublic) - continue; - result[MapClrMemberNameToJsonFieldName(fieldInfo.Name)] = ReflectionUtils.GetGetMethod(fieldInfo); - } - return result; - } - - internal virtual IDictionary> SetterValueFactory(Type type) - { - IDictionary> result = new Dictionary>(); - foreach (PropertyInfo propertyInfo in ReflectionUtils.GetProperties(type)) - { - if (propertyInfo.CanWrite) - { - MethodInfo setMethod = ReflectionUtils.GetSetterMethodInfo(propertyInfo); - if (setMethod.IsStatic || !setMethod.IsPublic) - continue; - result[MapClrMemberNameToJsonFieldName(propertyInfo.Name)] = new KeyValuePair(propertyInfo.PropertyType, ReflectionUtils.GetSetMethod(propertyInfo)); - } - } - foreach (FieldInfo fieldInfo in ReflectionUtils.GetFields(type)) - { - if (fieldInfo.IsInitOnly || fieldInfo.IsStatic || !fieldInfo.IsPublic) - continue; - result[MapClrMemberNameToJsonFieldName(fieldInfo.Name)] = new KeyValuePair(fieldInfo.FieldType, ReflectionUtils.GetSetMethod(fieldInfo)); - } - return result; - } - - public virtual bool TrySerializeNonPrimitiveObject(object input, out object output) - { - return TrySerializeKnownTypes(input, out output) || TrySerializeUnknownTypes(input, out output); - } - - [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")] - public virtual object DeserializeObject(object value, Type type) - { - if (type == null) throw new ArgumentNullException("type"); - string str = value as string; - - if (type == typeof (Guid) && string.IsNullOrEmpty(str)) - return default(Guid); - - if (value == null) - return null; - - object obj = null; - - if (str != null) - { - if (str.Length != 0) // We know it can't be null now. - { - if (type == typeof(DateTime) || (ReflectionUtils.IsNullableType(type) && Nullable.GetUnderlyingType(type) == typeof(DateTime))) - return DateTime.ParseExact(str, Iso8601Format, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal); -#if !PocketPC - if (type == typeof(DateTimeOffset) || (ReflectionUtils.IsNullableType(type) && Nullable.GetUnderlyingType(type) == typeof(DateTimeOffset))) - return DateTimeOffset.ParseExact(str, Iso8601Format, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal); -#endif - if (type == typeof(Guid) || (ReflectionUtils.IsNullableType(type) && Nullable.GetUnderlyingType(type) == typeof(Guid))) - return new Guid(str); - return str; - } - else - { - if (type == typeof(Guid)) - obj = default(Guid); - else if (ReflectionUtils.IsNullableType(type) && Nullable.GetUnderlyingType(type) == typeof(Guid)) - obj = null; - else - obj = str; - } - // Empty string case - if (!ReflectionUtils.IsNullableType(type) && Nullable.GetUnderlyingType(type) == typeof(Guid)) - return str; - } - else if (value is bool) - return value; - - bool valueIsLong = value is long; - bool valueIsDouble = value is double; - if ((valueIsLong && type == typeof(long)) || (valueIsDouble && type == typeof(double))) - return value; - if ((valueIsDouble && type != typeof(double)) || (valueIsLong && type != typeof(long))) - { - obj = type == typeof(int) || type == typeof(long) || type == typeof(double) || type == typeof(float) || type == typeof(bool) || type == typeof(decimal) || type == typeof(byte) || type == typeof(short) - ? Convert.ChangeType(value, type, CultureInfo.InvariantCulture) - : value; - } - else - { - IDictionary objects = value as IDictionary; - if (objects != null) - { - IDictionary jsonObject = objects; - - if (ReflectionUtils.IsTypeDictionary(type)) - { - // if dictionary then - Type[] types = ReflectionUtils.GetGenericTypeArguments(type); - Type keyType = types[0]; - Type valueType = types[1]; - - Type genericType = typeof(Dictionary<,>).MakeGenericType(keyType, valueType); - - IDictionary dict = (IDictionary)ConstructorCache[genericType](); - - foreach (KeyValuePair kvp in jsonObject) - dict.Add(kvp.Key, DeserializeObject(kvp.Value, valueType)); - - obj = dict; - } - else - { - if (type == typeof(object)) - obj = value; - else - { - obj = ConstructorCache[type](); - foreach (KeyValuePair> setter in SetCache[type]) - { - object jsonValue; - if (jsonObject.TryGetValue(setter.Key, out jsonValue)) - { - jsonValue = DeserializeObject(jsonValue, setter.Value.Key); - setter.Value.Value(obj, jsonValue); - } - } - } - } - } - else - { - IList valueAsList = value as IList; - if (valueAsList != null) - { - IList jsonObject = valueAsList; - IList list = null; - - if (type.IsArray) - { - list = (IList)ConstructorCache[type](jsonObject.Count); - int i = 0; - foreach (object o in jsonObject) - list[i++] = DeserializeObject(o, type.GetElementType()); - } - else if (ReflectionUtils.IsTypeGenericeCollectionInterface(type) || ReflectionUtils.IsAssignableFrom(typeof(IList), type)) - { - Type innerType = ReflectionUtils.GetGenericTypeArguments(type)[0]; - Type genericType = typeof(List<>).MakeGenericType(innerType); - list = (IList)ConstructorCache[genericType](jsonObject.Count); - foreach (object o in jsonObject) - list.Add(DeserializeObject(o, innerType)); - } - obj = list; - } - } - return obj; - } - if (ReflectionUtils.IsNullableType(type)) - return ReflectionUtils.ToNullableType(obj, type); - return obj; - } - - protected virtual object SerializeEnum(Enum p) - { - return Convert.ToDouble(p, CultureInfo.InvariantCulture); - } - - [SuppressMessage("Microsoft.Design", "CA1007:UseGenericsWhereAppropriate", Justification="Need to support .NET 2")] - protected virtual bool TrySerializeKnownTypes(object input, out object output) - { - bool returnValue = true; - if (input is DateTime) - output = ((DateTime)input).ToUniversalTime().ToString(Iso8601Format[0], CultureInfo.InvariantCulture); -#if !PocketPC - else if (input is DateTimeOffset) - output = ((DateTimeOffset)input).ToUniversalTime().ToString(Iso8601Format[0], CultureInfo.InvariantCulture); -#endif - else if (input is Guid) - output = ((Guid)input).ToString("D"); - else if (input is Uri) - output = input.ToString(); - else - { - Enum inputEnum = input as Enum; - if (inputEnum != null) - output = SerializeEnum(inputEnum); - else - { - returnValue = false; - output = null; - } - } - return returnValue; - } - [SuppressMessage("Microsoft.Design", "CA1007:UseGenericsWhereAppropriate", Justification="Need to support .NET 2")] - protected virtual bool TrySerializeUnknownTypes(object input, out object output) - { - if (input == null) throw new ArgumentNullException("input"); - output = null; - Type type = input.GetType(); - if (type.FullName == null) - return false; - IDictionary obj = new JsonObject(); - IDictionary getters = GetCache[type]; - foreach (KeyValuePair getter in getters) - { - if (getter.Value != null) - obj.Add(MapClrMemberNameToJsonFieldName(getter.Key), getter.Value(input)); - } - output = obj; - return true; - } - } - -#if SIMPLE_JSON_DATACONTRACT - [GeneratedCode("simple-json", "1.0.0")] -#if SIMPLE_JSON_INTERNAL - internal -#else - public -#endif - class DataContractJsonSerializerStrategy : PocoJsonSerializerStrategy - { - public DataContractJsonSerializerStrategy() - { - GetCache = new ReflectionUtils.ThreadSafeDictionary>(GetterValueFactory); - SetCache = new ReflectionUtils.ThreadSafeDictionary>>(SetterValueFactory); - } - - internal override IDictionary GetterValueFactory(Type type) - { - bool hasDataContract = ReflectionUtils.GetAttribute(type, typeof(DataContractAttribute)) != null; - if (!hasDataContract) - return base.GetterValueFactory(type); - string jsonKey; - IDictionary result = new Dictionary(); - foreach (PropertyInfo propertyInfo in ReflectionUtils.GetProperties(type)) - { - if (propertyInfo.CanRead) - { - MethodInfo getMethod = ReflectionUtils.GetGetterMethodInfo(propertyInfo); - if (!getMethod.IsStatic && CanAdd(propertyInfo, out jsonKey)) - result[jsonKey] = ReflectionUtils.GetGetMethod(propertyInfo); - } - } - foreach (FieldInfo fieldInfo in ReflectionUtils.GetFields(type)) - { - if (!fieldInfo.IsStatic && CanAdd(fieldInfo, out jsonKey)) - result[jsonKey] = ReflectionUtils.GetGetMethod(fieldInfo); - } - return result; - } - - internal override IDictionary> SetterValueFactory(Type type) - { - bool hasDataContract = ReflectionUtils.GetAttribute(type, typeof(DataContractAttribute)) != null; - if (!hasDataContract) - return base.SetterValueFactory(type); - string jsonKey; - IDictionary> result = new Dictionary>(); - foreach (PropertyInfo propertyInfo in ReflectionUtils.GetProperties(type)) - { - if (propertyInfo.CanWrite) - { - MethodInfo setMethod = ReflectionUtils.GetSetterMethodInfo(propertyInfo); - if (!setMethod.IsStatic && CanAdd(propertyInfo, out jsonKey)) - result[jsonKey] = new KeyValuePair(propertyInfo.PropertyType, ReflectionUtils.GetSetMethod(propertyInfo)); - } - } - foreach (FieldInfo fieldInfo in ReflectionUtils.GetFields(type)) - { - if (!fieldInfo.IsInitOnly && !fieldInfo.IsStatic && CanAdd(fieldInfo, out jsonKey)) - result[jsonKey] = new KeyValuePair(fieldInfo.FieldType, ReflectionUtils.GetSetMethod(fieldInfo)); - } - // todo implement sorting for DATACONTRACT. - return result; - } - - private static bool CanAdd(MemberInfo info, out string jsonKey) - { - jsonKey = null; - if (ReflectionUtils.GetAttribute(info, typeof(IgnoreDataMemberAttribute)) != null) - return false; - DataMemberAttribute dataMemberAttribute = (DataMemberAttribute)ReflectionUtils.GetAttribute(info, typeof(DataMemberAttribute)); - if (dataMemberAttribute == null) - return false; - jsonKey = string.IsNullOrEmpty(dataMemberAttribute.Name) ? info.Name : dataMemberAttribute.Name; - return true; - } - } - -#endif - - namespace Reflection - { - // This class is meant to be copied into other libraries. So we want to exclude it from Code Analysis rules - // that might be in place in the target project. - [GeneratedCode("reflection-utils", "1.0.0")] -#if SIMPLE_JSON_REFLECTION_UTILS_PUBLIC - public -#else - internal -#endif - class ReflectionUtils - { - private static readonly object[] EmptyObjects = new object[] { }; - - public delegate object GetDelegate(object source); - public delegate void SetDelegate(object source, object value); - public delegate object ConstructorDelegate(params object[] args); - - public delegate TValue ThreadSafeDictionaryValueFactory(TKey key); - - public static Attribute GetAttribute(MemberInfo info, Type type) - { -#if SIMPLE_JSON_TYPEINFO - if (info == null || type == null || !info.IsDefined(type)) - return null; - return info.GetCustomAttribute(type); -#else - if (info == null || type == null || !Attribute.IsDefined(info, type)) - return null; - return Attribute.GetCustomAttribute(info, type); -#endif - } - - public static Attribute GetAttribute(Type objectType, Type attributeType) - { - -#if SIMPLE_JSON_TYPEINFO - if (objectType == null || attributeType == null || !objectType.GetTypeInfo().IsDefined(attributeType)) - return null; - return objectType.GetTypeInfo().GetCustomAttribute(attributeType); -#else - if (objectType == null || attributeType == null || !Attribute.IsDefined(objectType, attributeType)) - return null; - return Attribute.GetCustomAttribute(objectType, attributeType); -#endif - } - - public static Type[] GetGenericTypeArguments(Type type) - { -#if SIMPLE_JSON_TYPEINFO - return type.GetTypeInfo().GenericTypeArguments; -#else - return type.GetGenericArguments(); -#endif - } - - public static bool IsTypeGenericeCollectionInterface(Type type) - { -#if SIMPLE_JSON_TYPEINFO - if (!type.GetTypeInfo().IsGenericType) -#else - if (!type.IsGenericType) -#endif - return false; - - Type genericDefinition = type.GetGenericTypeDefinition(); - - return (genericDefinition == typeof(IList<>) || genericDefinition == typeof(ICollection<>) || genericDefinition == typeof(IEnumerable<>)); - } - - public static bool IsAssignableFrom(Type type1, Type type2) - { -#if SIMPLE_JSON_TYPEINFO - return type1.GetTypeInfo().IsAssignableFrom(type2.GetTypeInfo()); -#else - return type1.IsAssignableFrom(type2); -#endif - } - - public static bool IsTypeDictionary(Type type) - { -#if SIMPLE_JSON_TYPEINFO - if (typeof(IDictionary<,>).GetTypeInfo().IsAssignableFrom(type.GetTypeInfo())) - return true; - - if (!type.GetTypeInfo().IsGenericType) - return false; -#else - if (typeof(System.Collections.IDictionary).IsAssignableFrom(type)) - return true; - - if (!type.IsGenericType) - return false; -#endif - Type genericDefinition = type.GetGenericTypeDefinition(); - return genericDefinition == typeof(IDictionary<,>); - } - - public static bool IsNullableType(Type type) - { - return -#if SIMPLE_JSON_TYPEINFO - type.GetTypeInfo().IsGenericType -#else - type.IsGenericType -#endif - && type.GetGenericTypeDefinition() == typeof(Nullable<>); - } - - public static object ToNullableType(object obj, Type nullableType) - { - return obj == null ? null : Convert.ChangeType(obj, Nullable.GetUnderlyingType(nullableType), CultureInfo.InvariantCulture); - } - - public static bool IsValueType(Type type) - { -#if SIMPLE_JSON_TYPEINFO - return type.GetTypeInfo().IsValueType; -#else - return type.IsValueType; -#endif - } - - public static IEnumerable GetConstructors(Type type) - { -#if SIMPLE_JSON_TYPEINFO - return type.GetTypeInfo().DeclaredConstructors; -#else - return type.GetConstructors(); -#endif - } - - public static ConstructorInfo GetConstructorInfo(Type type, params Type[] argsType) - { - IEnumerable constructorInfos = GetConstructors(type); - int i; - bool matches; - foreach (ConstructorInfo constructorInfo in constructorInfos) - { - ParameterInfo[] parameters = constructorInfo.GetParameters(); - if (argsType.Length != parameters.Length) - continue; - - i = 0; - matches = true; - foreach (ParameterInfo parameterInfo in constructorInfo.GetParameters()) - { - if (parameterInfo.ParameterType != argsType[i]) - { - matches = false; - break; - } - } - - if (matches) - return constructorInfo; - } - - return null; - } - - public static IEnumerable GetProperties(Type type) - { -#if SIMPLE_JSON_TYPEINFO - return type.GetTypeInfo().DeclaredProperties; -#else - return type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static); -#endif - } - - public static IEnumerable GetFields(Type type) - { -#if SIMPLE_JSON_TYPEINFO - return type.GetTypeInfo().DeclaredFields; -#else - return type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static); -#endif - } - - public static MethodInfo GetGetterMethodInfo(PropertyInfo propertyInfo) - { -#if SIMPLE_JSON_TYPEINFO - return propertyInfo.GetMethod; -#else - return propertyInfo.GetGetMethod(true); -#endif - } - - public static MethodInfo GetSetterMethodInfo(PropertyInfo propertyInfo) - { -#if SIMPLE_JSON_TYPEINFO - return propertyInfo.SetMethod; -#else - return propertyInfo.GetSetMethod(true); -#endif - } - - public static ConstructorDelegate GetContructor(ConstructorInfo constructorInfo) - { -#if SIMPLE_JSON_NO_LINQ_EXPRESSION - return GetConstructorByReflection(constructorInfo); -#else - return GetConstructorByExpression(constructorInfo); -#endif - } - - public static ConstructorDelegate GetContructor(Type type, params Type[] argsType) - { -#if SIMPLE_JSON_NO_LINQ_EXPRESSION - return GetConstructorByReflection(type, argsType); -#else - return GetConstructorByExpression(type, argsType); -#endif - } - - public static ConstructorDelegate GetConstructorByReflection(ConstructorInfo constructorInfo) - { - return delegate(object[] args) { return constructorInfo.Invoke(args); }; - } - - public static ConstructorDelegate GetConstructorByReflection(Type type, params Type[] argsType) - { - ConstructorInfo constructorInfo = GetConstructorInfo(type, argsType); - return constructorInfo == null ? null : GetConstructorByReflection(constructorInfo); - } - -#if !SIMPLE_JSON_NO_LINQ_EXPRESSION - - public static ConstructorDelegate GetConstructorByExpression(ConstructorInfo constructorInfo) - { - ParameterInfo[] paramsInfo = constructorInfo.GetParameters(); - ParameterExpression param = Expression.Parameter(typeof(object[]), "args"); - Expression[] argsExp = new Expression[paramsInfo.Length]; - for (int i = 0; i < paramsInfo.Length; i++) - { - Expression index = Expression.Constant(i); - Type paramType = paramsInfo[i].ParameterType; - Expression paramAccessorExp = Expression.ArrayIndex(param, index); - Expression paramCastExp = Expression.Convert(paramAccessorExp, paramType); - argsExp[i] = paramCastExp; - } - NewExpression newExp = Expression.New(constructorInfo, argsExp); - Expression> lambda = Expression.Lambda>(newExp, param); - Func compiledLambda = lambda.Compile(); - return delegate(object[] args) { return compiledLambda(args); }; - } - - public static ConstructorDelegate GetConstructorByExpression(Type type, params Type[] argsType) - { - ConstructorInfo constructorInfo = GetConstructorInfo(type, argsType); - return constructorInfo == null ? null : GetConstructorByExpression(constructorInfo); - } - -#endif - - public static GetDelegate GetGetMethod(PropertyInfo propertyInfo) - { -#if SIMPLE_JSON_NO_LINQ_EXPRESSION || PocketPC - return GetGetMethodByReflection(propertyInfo); -#else - return GetGetMethodByExpression(propertyInfo); -#endif - } - - public static GetDelegate GetGetMethod(FieldInfo fieldInfo) - { -#if SIMPLE_JSON_NO_LINQ_EXPRESSION || PocketPC - return GetGetMethodByReflection(fieldInfo); -#else - return GetGetMethodByExpression(fieldInfo); -#endif - } - - public static GetDelegate GetGetMethodByReflection(PropertyInfo propertyInfo) - { - MethodInfo methodInfo = GetGetterMethodInfo(propertyInfo); - return delegate(object source) { return methodInfo.Invoke(source, EmptyObjects); }; - } - - public static GetDelegate GetGetMethodByReflection(FieldInfo fieldInfo) - { - return delegate(object source) { return fieldInfo.GetValue(source); }; - } - -#if !SIMPLE_JSON_NO_LINQ_EXPRESSION && !PocketPC - - public static GetDelegate GetGetMethodByExpression(PropertyInfo propertyInfo) - { - MethodInfo getMethodInfo = GetGetterMethodInfo(propertyInfo); - ParameterExpression instance = Expression.Parameter(typeof(object), "instance"); - UnaryExpression instanceCast = (!IsValueType(propertyInfo.DeclaringType)) ? Expression.TypeAs(instance, propertyInfo.DeclaringType) : Expression.Convert(instance, propertyInfo.DeclaringType); - Func compiled = Expression.Lambda>(Expression.TypeAs(Expression.Call(instanceCast, getMethodInfo), typeof(object)), instance).Compile(); - return delegate(object source) { return compiled(source); }; - } - - public static GetDelegate GetGetMethodByExpression(FieldInfo fieldInfo) - { - ParameterExpression instance = Expression.Parameter(typeof(object), "instance"); - MemberExpression member = Expression.Field(Expression.Convert(instance, fieldInfo.DeclaringType), fieldInfo); - GetDelegate compiled = Expression.Lambda(Expression.Convert(member, typeof(object)), instance).Compile(); - return delegate(object source) { return compiled(source); }; - } - -#endif - - public static SetDelegate GetSetMethod(PropertyInfo propertyInfo) - { -#if SIMPLE_JSON_NO_LINQ_EXPRESSION - return GetSetMethodByReflection(propertyInfo); -#else - return GetSetMethodByExpression(propertyInfo); -#endif - } - - public static SetDelegate GetSetMethod(FieldInfo fieldInfo) - { -#if SIMPLE_JSON_NO_LINQ_EXPRESSION - return GetSetMethodByReflection(fieldInfo); -#else - return GetSetMethodByExpression(fieldInfo); -#endif - } - - public static SetDelegate GetSetMethodByReflection(PropertyInfo propertyInfo) - { - MethodInfo methodInfo = GetSetterMethodInfo(propertyInfo); - return delegate(object source, object value) { methodInfo.Invoke(source, new object[] { value }); }; - } - - public static SetDelegate GetSetMethodByReflection(FieldInfo fieldInfo) - { - return delegate(object source, object value) { fieldInfo.SetValue(source, value); }; - } - -#if !SIMPLE_JSON_NO_LINQ_EXPRESSION - - public static SetDelegate GetSetMethodByExpression(PropertyInfo propertyInfo) - { - MethodInfo setMethodInfo = GetSetterMethodInfo(propertyInfo); - ParameterExpression instance = Expression.Parameter(typeof(object), "instance"); - ParameterExpression value = Expression.Parameter(typeof(object), "value"); - UnaryExpression instanceCast = (!IsValueType(propertyInfo.DeclaringType)) ? Expression.TypeAs(instance, propertyInfo.DeclaringType) : Expression.Convert(instance, propertyInfo.DeclaringType); - UnaryExpression valueCast = (!IsValueType(propertyInfo.PropertyType)) ? Expression.TypeAs(value, propertyInfo.PropertyType) : Expression.Convert(value, propertyInfo.PropertyType); - Action compiled = Expression.Lambda>(Expression.Call(instanceCast, setMethodInfo, valueCast), new ParameterExpression[] { instance, value }).Compile(); - return delegate(object source, object val) { compiled(source, val); }; - } - - public static SetDelegate GetSetMethodByExpression(FieldInfo fieldInfo) - { - ParameterExpression instance = Expression.Parameter(typeof(object), "instance"); - ParameterExpression value = Expression.Parameter(typeof(object), "value"); - Action compiled = Expression.Lambda>( - Assign(Expression.Field(Expression.Convert(instance, fieldInfo.DeclaringType), fieldInfo), Expression.Convert(value, fieldInfo.FieldType)), instance, value).Compile(); - return delegate(object source, object val) { compiled(source, val); }; - } - - public static BinaryExpression Assign(Expression left, Expression right) - { -#if SIMPLE_JSON_TYPEINFO - return Expression.Assign(left, right); -#else - MethodInfo assign = typeof(Assigner<>).MakeGenericType(left.Type).GetMethod("Assign"); - BinaryExpression assignExpr = Expression.Add(left, right, assign); - return assignExpr; -#endif - } - - private static class Assigner - { - public static T Assign(ref T left, T right) - { - return (left = right); - } - } - -#endif - - public sealed class ThreadSafeDictionary : IDictionary - { - private readonly object _lock = new object(); - private readonly ThreadSafeDictionaryValueFactory _valueFactory; - private Dictionary _dictionary; - - public ThreadSafeDictionary(ThreadSafeDictionaryValueFactory valueFactory) - { - _valueFactory = valueFactory; - } - - private TValue Get(TKey key) - { - if (_dictionary == null) - return AddValue(key); - TValue value; - if (!_dictionary.TryGetValue(key, out value)) - return AddValue(key); - return value; - } - - private TValue AddValue(TKey key) - { - TValue value = _valueFactory(key); - lock (_lock) - { - if (_dictionary == null) - { - _dictionary = new Dictionary(); - _dictionary[key] = value; - } - else - { - TValue val; - if (_dictionary.TryGetValue(key, out val)) - return val; - Dictionary dict = new Dictionary(_dictionary); - dict[key] = value; - _dictionary = dict; - } - } - return value; - } - - public void Add(TKey key, TValue value) - { - throw new NotImplementedException(); - } - - public bool ContainsKey(TKey key) - { - return _dictionary.ContainsKey(key); - } - - public ICollection Keys - { - get { return _dictionary.Keys; } - } - - public bool Remove(TKey key) - { - throw new NotImplementedException(); - } - - public bool TryGetValue(TKey key, out TValue value) - { - value = this[key]; - return true; - } - - public ICollection Values - { - get { return _dictionary.Values; } - } - - public TValue this[TKey key] - { - get { return Get(key); } - set { throw new NotImplementedException(); } - } - - public void Add(KeyValuePair item) - { - throw new NotImplementedException(); - } - - public void Clear() - { - throw new NotImplementedException(); - } - - public bool Contains(KeyValuePair item) - { - throw new NotImplementedException(); - } - - public void CopyTo(KeyValuePair[] array, int arrayIndex) - { - throw new NotImplementedException(); - } - - public int Count - { - get { return _dictionary.Count; } - } - - public bool IsReadOnly - { - get { throw new NotImplementedException(); } - } - - public bool Remove(KeyValuePair item) - { - throw new NotImplementedException(); - } - - public IEnumerator> GetEnumerator() - { - return _dictionary.GetEnumerator(); - } - - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() - { - return _dictionary.GetEnumerator(); - } - } - - } - } -} -// ReSharper restore LoopCanBeConvertedToQuery -// ReSharper restore RedundantExplicitArrayCreation -// ReSharper restore SuggestUseVarKeywordEvident diff --git a/RestSharp/T4Helper/T4Helper.tt b/RestSharp/T4Helper/T4Helper.tt deleted file mode 100644 index 532f7d9c4..000000000 --- a/RestSharp/T4Helper/T4Helper.tt +++ /dev/null @@ -1,257 +0,0 @@ -<#@ template language="C#v3.5" hostspecific="True" #> -<#@ output extension="log" #> -<#@ include file="T4Toolbox.tt" #> -<#@ assembly name="System.Core" #> -<#@ assembly name="System.Xml.Linq" #> -<#@ import namespace="System" #> -<#@ import namespace="System.Collections.Generic" #> -<#@ import namespace="System.Globalization" #> -<#@ import namespace="System.IO" #> -<#@ import namespace="System.Xml.Linq" #> -<#@ import namespace="System.Linq" #> -<#@ import namespace="System.Text.RegularExpressions" #> -<# - // Requires T4Toolbox from http://t4toolbox.codeplex.com/ to be installed - // Instructions: - // - Place this file and T4Toolbox.tt in a directory - // - Select T4Toolbox.tt, go to the file Properties and clear out 'Custom Tool' - // - Create an .rs file with sample XML for the class you want to generate - // - Save this file to generate .cs file from .rs file or right click and select 'Run Custom Tool' - // - Create dummy data in .rs file to get desired output - // - Optionally set _namespace variable - // \/ \/ \/ \/ \/ - var _namespace = ""; - - var currentDirectory = Path.GetDirectoryName(Host.TemplateFile); - var restSharpSchemas = Directory.GetFiles(currentDirectory, "*.rs", SearchOption.AllDirectories); - - foreach (var file in restSharpSchemas) - { - string newFile = Path.Combine(Path.GetDirectoryName(file), Path.GetFileNameWithoutExtension(file) + ".cs"); - RestSharpTemplate template = new RestSharpTemplate(file, _namespace); - template.Output.File = newFile; - template.Render(); - } -#> - -<#+ -public class RestSharpTemplate : T4Toolbox.Template -{ - protected string FilePath { get; set; } - protected string Namespace { get; set; } - public RestSharpTemplate(string file, string _namespace) - { - FilePath = file; - Namespace = _namespace; - } - - public override string TransformText() - { - // load file - var doc = XDocument.Load(FilePath); - RemoveNamespace(doc); - - // write header - Write("using System;\r\n"); - Write("using System.Collections.Generic;"); - - if (!string.IsNullOrEmpty(Namespace)) - Write(Environment.NewLine + "namespace " + Namespace + " {" + Environment.NewLine); - - Write(BuildClass(doc.Root)); - - foreach (var item in Deferred.Reverse()) - Write(item.Value); - - if (!string.IsNullOrEmpty(Namespace)) - Write(Environment.NewLine + "}"); - - return this.GenerationEnvironment.ToString(); - } - - string BuildClass(XElement root) - { - return BuildClass(root, false); - } - - string BuildClass(XElement root, bool includeValueProp) - { - string valueProp = ""; - if (includeValueProp) { - valueProp = string.Format(" public {0} Value {{ get; set; }}{1}", GuessType(root.Value), Environment.NewLine); - } - - string classTemplate = @" -public class {0} {{ -{1}{2}}}"; - - System.Text.StringBuilder builder = new System.Text.StringBuilder(); - - if (HasList(root)) { - AddListWithReference(root, builder); - } - else { - builder.Append(BuildProperties(root)); - } - return string.Format(classTemplate, ToPascalCase(root.Name), builder.ToString(), valueProp); - } - - Dictionary Deferred = new Dictionary(); - - string BuildProperties(XElement root) - { - var builder = new System.Text.StringBuilder(); - - foreach (var el in root.Elements()) { - if (el.HasElements) { - if (HasList(el)) { - AddListWithReference(el, builder); - } - else { - AddClassWithReference(el, builder, false); - } - - continue; - } - - if (!el.HasElements && !el.HasAttributes) { - builder.Append(BuildProperty(el)); - continue; - } - - if ((!el.HasElements && el.HasAttributes)) { - bool includeValueProp = el.Value != null; - - AddClassWithReference(el, builder, includeValueProp); - continue; - } - - - } - - builder.Append(BuildAttributes(root)); - - return builder.ToString(); - } - - bool HasList(XElement el) - { - return el.Elements().Count() > 1 && el.Elements().All(e => e.Name == el.Elements().First().Name); - } - - void AddListWithReference(XElement el, System.Text.StringBuilder builder) - { - var first = el.Elements().First(); - if (!Deferred.ContainsKey(first.Name.ToString())) { - Deferred.Add(first.Name.ToString(), BuildClass(first)); - } - builder.AppendFormat(" public List<{0}.{1}> {2} {{ get; set; }}{3}", Namespace, ToPascalCase(first.Name), ToPascalCase(el.Name), Environment.NewLine); - } - - void AddClassWithReference(XElement el, System.Text.StringBuilder builder, bool includeValueProp) - { - var name = el.Name.ToString(); - if (!Deferred.ContainsKey(name)) { - Deferred.Add(name, BuildClass(el, includeValueProp)); - } - builder.AppendFormat(" public {0}.{1} {1} {{ get; set; }}{2}", Namespace, ToPascalCase(el.Name), Environment.NewLine); - } - - string BuildProperty(XElement root) - { - return string.Format(" public {0} {1} {{ get; set; }}{2}", GuessType(root.Value), ToPascalCase(root.Name), Environment.NewLine); - } - - string BuildAttributes(XElement root) - { - var builder = new System.Text.StringBuilder(); - foreach (var at in root.Attributes()) { - builder.AppendFormat(" public {0} {1} {{ get; set; }}{2}", GuessType(at.Value), ToPascalCase(at.Name), Environment.NewLine); - } - - return builder.ToString(); - } - - string GuessType(string value) - { - bool boolVal; - if (bool.TryParse(value, out boolVal)) return typeof(bool).ToString(); - - int intVal; - if (int.TryParse(value, out intVal)) return typeof(int).ToString(); - - long longVal; - if (long.TryParse(value, out longVal)) return typeof(long).ToString(); - - decimal decVal; - if (decimal.TryParse(value, out decVal)) return typeof(decimal).ToString(); - - var dateFormats = new string[] { - "u", - "s", - "yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'", - "yyyy-MM-ddTHH:mm:ssZ", - "yyyy-MM-dd HH:mm:ssZ", - "yyyy-MM-ddTHH:mm:ss", - "yyyy-MM-ddTHH:mm:sszzzzzz" - }; - - DateTime dateVal; - if (DateTime.TryParseExact(value, dateFormats, - System.Globalization.CultureInfo.InvariantCulture, - System.Globalization.DateTimeStyles.None, out dateVal)) { - return typeof(DateTime).ToString(); - } - - // parse TimeSpan? - - return typeof(string).ToString(); - } - - void RemoveNamespace(XDocument xdoc) - { - foreach (XElement e in xdoc.Root.DescendantsAndSelf()) - { - if (e.Name.Namespace != XNamespace.None) - { - e.Name = XNamespace.None.GetName(e.Name.LocalName); - } - if (e.Attributes().Where(a => a.IsNamespaceDeclaration || a.Name.Namespace != XNamespace.None).Any()) - { - e.ReplaceAttributes(e.Attributes().Select(a => a.IsNamespaceDeclaration ? null : a.Name.Namespace != XNamespace.None ? new XAttribute(XNamespace.None.GetName(a.Name.LocalName), a.Value) : a)); - } - } - } - - string ToPascalCase(XName xname) { - string text = xname.LocalName; - bool removeUnderscores = true; - if (String.IsNullOrEmpty(text)) - return text; - - text = text.Replace("_", " "); - string joinString = removeUnderscores ? String.Empty : "_"; - string[] words = text.Split(' '); - if (words.Length > 1 || IsUpperCase(words[0])) { - for (int i = 0; i < words.Length; i++) { - if (words[i].Length > 0) { - string word = words[i]; - string restOfWord = word.Substring(1); - - if (IsUpperCase(restOfWord)) - restOfWord = restOfWord.ToLower(CultureInfo.CurrentUICulture); - - char firstChar = char.ToUpper(word[0], CultureInfo.CurrentUICulture); - words[i] = String.Concat(firstChar, restOfWord); - } - } - return String.Join(joinString, words); - } - return String.Concat(words[0].Substring(0, 1).ToUpper(CultureInfo.CurrentUICulture), words[0].Substring(1)); - } - bool IsUpperCase(string inputString) { - return Regex.IsMatch(inputString, @"^[A-Z]+$"); - } - -} -#> \ No newline at end of file diff --git a/RestSharp/T4Helper/T4Toolbox.tt b/RestSharp/T4Helper/T4Toolbox.tt deleted file mode 100644 index 2e42ea7d0..000000000 --- a/RestSharp/T4Helper/T4Toolbox.tt +++ /dev/null @@ -1,9 +0,0 @@ -<#@ dte processor="T4Toolbox.DteProcessor" #> -<#@ TransformationContext processor="T4Toolbox.TransformationContextProcessor" #> -<#@ assembly name="System.Xml" #> -<#@ assembly name="EnvDTE" #> -<#@ assembly name="Microsoft.VisualStudio.Ole.Interop" #> -<#@ assembly name="Microsoft.VisualStudio.Shell" #> -<#@ assembly name="Microsoft.VisualStudio.Shell.Interop" #> -<#@ assembly name="VSLangProj" #> -<#@ import namespace="T4Toolbox" #> \ No newline at end of file diff --git a/RestSharp/Validation/Require.cs b/RestSharp/Validation/Require.cs deleted file mode 100644 index 63fa37376..000000000 --- a/RestSharp/Validation/Require.cs +++ /dev/null @@ -1,37 +0,0 @@ -#region License -// Copyright 2010 John Sheehan -// -// 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. -#endregion - -using System; - -namespace RestSharp.Validation -{ - /// - /// Helper methods for validating required values - /// - public class Require - { - /// - /// Require a parameter to not be null - /// - /// Name of the parameter - /// Value of the parameter - public static void Argument(string argumentName, object argumentValue) { - if (argumentValue == null) { - throw new ArgumentException("Argument cannot be null.", argumentName); - } - } - } -} diff --git a/RestSharp/Validation/Validate.cs b/RestSharp/Validation/Validate.cs deleted file mode 100644 index b23ce975f..000000000 --- a/RestSharp/Validation/Validate.cs +++ /dev/null @@ -1,52 +0,0 @@ -#region License -// Copyright 2010 John Sheehan -// -// 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. -#endregion - -using System; - -namespace RestSharp.Validation -{ - /// - /// Helper methods for validating values - /// - public class Validate - { - /// - /// Validate an integer value is between the specified values (exclusive of min/max) - /// - /// Value to validate - /// Exclusive minimum value - /// Exclusive maximum value - public static void IsBetween(int value, int min, int max) { - if (value < min || value > max) { - throw new ArgumentException(string.Format("Value ({0}) is not between {1} and {2}.", value, min, max)); - } - } - - /// - /// Validate a string length - /// - /// String to be validated - /// Maximum length of the string - public static void IsValidLength(string value, int maxSize) { - if (value == null) - return; - - if (value.Length > maxSize) { - throw new ArgumentException(string.Format("String is longer than max allowed size ({0}).", maxSize)); - } - } - } -} diff --git a/RestSharp/packages.config b/RestSharp/packages.config deleted file mode 100644 index 8cdf329ed..000000000 --- a/RestSharp/packages.config +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/Tools/NuGet.exe b/Tools/NuGet.exe deleted file mode 100644 index 2c9369842..000000000 Binary files a/Tools/NuGet.exe and /dev/null differ diff --git a/agents.md b/agents.md new file mode 100644 index 000000000..5093f99f8 --- /dev/null +++ b/agents.md @@ -0,0 +1,671 @@ +### 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, testing details, source generation, and conventions unique to this repository. + +--- + +## Solution Structure + +### Projects and Organization + +The solution (`RestSharp.sln`) is organized into the following structure: + +**Core Library:** +- `src/RestSharp/` - Main library targeting multiple frameworks + +**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 + +--- + +## Build and Configuration + +### Multi-Targeting + +**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 + +**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 + +**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 NET` +- 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 + +**Symbol Packages:** `.snupkg` format for debugging + +**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` + +--- + +## 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 + +- `.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 + +**Nullable Reference Types:** +- Enabled in source projects (`Nullable=enable`) +- Disabled in test projects (`Nullable=disable`) + +**Language Version:** `preview` - allows use of latest C# features + +**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 +``` + +--- + +## 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 + +# 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.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 diff --git a/benchmarks/RestSharp.Benchmarks/Program.cs b/benchmarks/RestSharp.Benchmarks/Program.cs new file mode 100644 index 000000000..6dedaf57e --- /dev/null +++ b/benchmarks/RestSharp.Benchmarks/Program.cs @@ -0,0 +1,4 @@ +using System.Reflection; +using BenchmarkDotNet.Running; + +BenchmarkSwitcher.FromAssembly(Assembly.GetExecutingAssembly()).Run(args); \ No newline at end of file diff --git a/benchmarks/RestSharp.Benchmarks/Requests/AddObjectToRequestParametersBenchmarks.cs b/benchmarks/RestSharp.Benchmarks/Requests/AddObjectToRequestParametersBenchmarks.cs new file mode 100644 index 000000000..355667196 --- /dev/null +++ b/benchmarks/RestSharp.Benchmarks/Requests/AddObjectToRequestParametersBenchmarks.cs @@ -0,0 +1,31 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Order; +using System.Globalization; + +namespace RestSharp.Benchmarks.Requests; + +[MemoryDiagnoser, RankColumn, Orderer(SummaryOrderPolicy.FastestToSlowest)] +public class AddObjectToRequestParametersBenchmarks { + Data _data; + + [GlobalSetup] + public void GlobalSetup() { + const string @string = "random string"; + const int arraySize = 10_000; + var strings = new string[arraySize]; + Array.Fill(strings, @string); + var ints = new int[arraySize]; + Array.Fill(ints, int.MaxValue); + + Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture; + var dateTime = DateTime.Parse("01/01/2013 03:03:12"); + + _data = new(@string, int.MaxValue, strings, ints, dateTime, strings); + } + + [Benchmark(Baseline = true)] + public void AddObject() => new RestRequest().AddObject(_data); + + [Benchmark] + public void AddObjectStatic() => new RestRequest().AddObjectStatic(_data); +} \ No newline at end of file diff --git a/benchmarks/RestSharp.Benchmarks/Requests/Data.cs b/benchmarks/RestSharp.Benchmarks/Requests/Data.cs new file mode 100644 index 000000000..48be615a7 --- /dev/null +++ b/benchmarks/RestSharp.Benchmarks/Requests/Data.cs @@ -0,0 +1,9 @@ +namespace RestSharp.Benchmarks.Requests; + +sealed record Data( + string String, + [property: RequestProperty(Name = "PropertyName")] int Int32, + string[] Strings, + [property: RequestProperty(Format = "00000", ArrayQueryType = RequestArrayQueryType.ArrayParameters)] int[] Ints, + [property: RequestProperty(Name = "DateTime", Format = "hh:mm tt")] object DateTime, + object StringArray); \ No newline at end of file diff --git a/benchmarks/RestSharp.Benchmarks/RestSharp.Benchmarks.csproj b/benchmarks/RestSharp.Benchmarks/RestSharp.Benchmarks.csproj new file mode 100644 index 000000000..3d939e0fb --- /dev/null +++ b/benchmarks/RestSharp.Benchmarks/RestSharp.Benchmarks.csproj @@ -0,0 +1,25 @@ + + + Exe + net9.0 + false + preview + enable + $([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildProjectDirectory), 'RestSharp.sln')) + + + + + + + + + + + + + + + + + diff --git a/benchmarks/RestSharp.Benchmarks/Serializers/JsonNetSerializeBenchmarks.cs b/benchmarks/RestSharp.Benchmarks/Serializers/JsonNetSerializeBenchmarks.cs new file mode 100644 index 000000000..7a8e40cdc --- /dev/null +++ b/benchmarks/RestSharp.Benchmarks/Serializers/JsonNetSerializeBenchmarks.cs @@ -0,0 +1,22 @@ +using AutoFixture; +using BenchmarkDotNet.Attributes; +using RestSharp.Serializers.NewtonsoftJson; + +namespace RestSharp.Benchmarks.Serializers; + +[MemoryDiagnoser] +public class JsonNetSerializeBenchmarks +{ + readonly JsonNetSerializer _serializer = new(); + + List _fakeData; + + [Params(1, 10, 20)] + public int N { get; set; } + + [GlobalSetup] + public void GlobalSetup() => _fakeData = new Fixture().CreateMany(N).ToList(); + + [Benchmark(Baseline = true)] + public string Serialize() => _serializer.Serialize(_fakeData); +} \ No newline at end of file diff --git a/benchmarks/RestSharp.Benchmarks/Serializers/TestClass.cs b/benchmarks/RestSharp.Benchmarks/Serializers/TestClass.cs new file mode 100644 index 000000000..7345517ab --- /dev/null +++ b/benchmarks/RestSharp.Benchmarks/Serializers/TestClass.cs @@ -0,0 +1,14 @@ +// ReSharper disable UnusedMember.Global +namespace RestSharp.Benchmarks.Serializers; + +public class TestClass { + public string SimpleString { get; set; } + public int SimpleInt { get; set; } + public List List { get; set; } + public Subclass Sub { get; set; } + + public class Subclass { + public string Thing { get; set; } + public int AnotherThing { get; set; } + } +} \ No newline at end of file diff --git a/build.bat b/build.bat deleted file mode 100644 index e9fab478a..000000000 --- a/build.bat +++ /dev/null @@ -1,63 +0,0 @@ -@echo Off -set config=%1 -if "%config%" == "" ( - set config=Release -) - -REM Build -REM %WINDIR%\Microsoft.NET\Framework\v4.0.30319\msbuild src\Twilio.sln /p:Configuration="%config%" /m /v:M /fl /flp:LogFile=msbuild.log;Verbosity=Normal /nr:true /p:BuildInParallel=true /p:RestorePackages=true /t:Rebuild -%WINDIR%\Microsoft.NET\Framework\v4.0.30319\msbuild.exe RestSharp.sln /p:Configuration=%config% /m /v:M /fl /flp:LogFile=msbuild.log;Verbosity=Normal /nr:true /p:BuildInParallel=true /p:RestorePackages=true /t:Clean,Rebuild -if not "%errorlevel%"=="0" goto failure - -REM Unit tests -REM "%GallioEcho%" RestSharp.IntegrationTests\bin\Release\RestSharp.IntegrationTests.dll -REM if not "%errorlevel%"=="0" goto failure - - -rd Download /s /q REM delete the old stuff - -if not exist Download\Net4 mkdir Download\Net4\ -if not exist Download\Silverlight mkdir Download\Silverlight\ -if not exist Download\WindowsPhone mkdir Download\WindowsPhone\ -if not exist Download\package\lib\net35 mkdir Download\package\lib\net35\ -if not exist Download\package\lib\net35-client mkdir Download\package\lib\net35-client\ -if not exist Download\package\lib\net4 mkdir Download\package\lib\net4\ -if not exist Download\package\lib\net4-client mkdir Download\package\lib\net4-client\ -if not exist Download\package\lib\sl4-wp71 mkdir Download\package\lib\sl4-wp71\ -if not exist Download\package\lib\sl4 mkdir Download\package\lib\sl4\ - -copy LICENSE.txt Download\ -copy readme.txt Download\package\ - -copy RestSharp\bin\Release\RestSharp.dll Download\Package\lib\net35\ -copy RestSharp\bin\Release\RestSharp.dll Download\Package\lib\net35-client\ - -copy RestSharp.Net4\bin\Release\RestSharp.dll Download\Package\lib\net4\ -copy RestSharp.Net4\bin\Release\RestSharp.dll Download\Package\lib\net4-client\ - -copy RestSharp.Silverlight\bin\Release\RestSharp.Silverlight.dll Download\Package\lib\sl4\ -copy RestSharp.WindowsPhone\bin\Release\RestSharp.WindowsPhone.dll Download\Package\lib\sl4-wp71\ - -copy RestSharp\bin\Release\RestSharp.xml Download\Package\lib\net35\ -copy RestSharp\bin\Release\RestSharp.xml Download\Package\lib\net35-client\ - -copy RestSharp.Net4\bin\Release\RestSharp.xml Download\Package\lib\net4\ -copy RestSharp.Net4\bin\Release\RestSharp.xml Download\Package\lib\net4-client\ - -copy RestSharp.Silverlight\bin\Release\RestSharp.Silverlight.xml Download\Package\lib\sl4\ -copy RestSharp.WindowsPhone\bin\Release\RestSharp.WindowsPhone.xml Download\Package\lib\sl4-wp71\ - -%nuget% pack "restsharp-computed.nuspec" -BasePath Download\Package -Output Download -if not "%errorlevel%"=="0" goto failure - -:success - -REM use github status API to indicate commit compile success - -exit 0 - -:failure - -REM use github status API to indicate commit compile success - -exit -1 diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 000000000..b2d6de306 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,20 @@ +# Dependencies +/node_modules + +# Production +/build + +# Generated files +.docusaurus +.cache-loader + +# Misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/docs/.idea/.gitignore b/docs/.idea/.gitignore new file mode 100644 index 000000000..13566b81b --- /dev/null +++ b/docs/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/docs/.idea/GitLink.xml b/docs/.idea/GitLink.xml new file mode 100644 index 000000000..009597cc2 --- /dev/null +++ b/docs/.idea/GitLink.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/docs/.idea/docs.iml b/docs/.idea/docs.iml new file mode 100644 index 000000000..24643cc37 --- /dev/null +++ b/docs/.idea/docs.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/.idea/material_theme_project_new.xml b/docs/.idea/material_theme_project_new.xml new file mode 100644 index 000000000..8cde3b428 --- /dev/null +++ b/docs/.idea/material_theme_project_new.xml @@ -0,0 +1,18 @@ + + + + + + + \ No newline at end of file diff --git a/docs/.idea/modules.xml b/docs/.idea/modules.xml new file mode 100644 index 000000000..6049cfe01 --- /dev/null +++ b/docs/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/docs/.idea/vcs.xml b/docs/.idea/vcs.xml new file mode 100644 index 000000000..6c0b86358 --- /dev/null +++ b/docs/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..80e81477a --- /dev/null +++ b/docs/README.md @@ -0,0 +1,26 @@ +# Website + +This is a RestSharp documentation website built using [Docusaurus](https://docusaurus.io/), a modern static website generator. + +### Installation + +``` +$ yarn +``` + +### Local Development + +``` +$ yarn start +``` + +This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. + +### Build + +``` +$ yarn build +``` + +This command generates static content into the `build` directory and can be served using any static contents hosting service. + diff --git a/docs/babel.config.js b/docs/babel.config.js new file mode 100644 index 000000000..e00595dae --- /dev/null +++ b/docs/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + presets: [require.resolve('@docusaurus/core/lib/babel/preset')], +}; diff --git a/docs/docs/advanced/_category_.json b/docs/docs/advanced/_category_.json new file mode 100644 index 000000000..f395bdfe4 --- /dev/null +++ b/docs/docs/advanced/_category_.json @@ -0,0 +1,7 @@ +{ + "label": "Advanced topics", + "position": 4, + "link": { + "type": "generated-index" + } +} diff --git a/docs/docs/advanced/authenticators.md b/docs/docs/advanced/authenticators.md new file mode 100644 index 000000000..14196d97c --- /dev/null +++ b/docs/docs/advanced/authenticators.md @@ -0,0 +1,185 @@ +# Authenticators + +RestSharp includes authenticators for basic HTTP, OAuth1 and token-based (JWT and OAuth2). + +There are two ways to set the authenticator: client-wide or per-request. + +Set the client-wide authenticator by assigning the `Authenticator` property of `RestClientOptions`: + +```csharp +var options = new RestClientOptions("https://example.com") { + Authenticator = new HttpBasicAuthenticator("username", "password") +}; +var client = new RestClient(options); +``` + +To set the authenticator per-request, assign the `Authenticator` property of `RestRequest`: + +```csharp +var request = new RestRequest("/api/users/me") { + Authenticator = new HttpBasicAuthenticator("username", "password") +}; +var response = await client.ExecuteAsync(request, cancellationToken); +``` + +## Basic authentication + +The `HttpBasicAuthenticator` allows you pass a username and password as a basic `Authorization` header using a base64 encoded string. + +```csharp +var options = new RestClientOptions("https://example.com") { + Authenticator = new HttpBasicAuthenticator("username", "password") +}; +var client = new RestClient(options); +``` + +## OAuth1 + +For OAuth1 authentication the `OAuth1Authenticator` class provides static methods to help generate an OAuth authenticator. +OAuth1 authenticator will add the necessary OAuth parameters to the request, including signature. + +The authenticator will use `HMAC SHA1` to create a signature by default. +Each static function to create the authenticator allows you to override the default and use another method to generate the signature. + +### Request token + +Getting a temporary request token is the usual first step in the 3-legged OAuth1 flow. +Use `OAuth1Authenticator.ForRequestToken` function to get the request token authenticator. +This method requires a `consumerKey` and `consumerSecret` to authenticate. + +```csharp +var options = new RestClientOptions("https://api.twitter.com") { + Authenticator = OAuth1Authenticator.ForRequestToken(consumerKey, consumerSecret) +}; +var client = new RestClient(options); +var request = new RestRequest("oauth/request_token"); +``` + +The response should contain the token and the token secret, which can then be used to complete the authorization process. +If you need to provide the callback URL, assign the `CallbackUrl` property of the authenticator to the callback destination. + +### Access token + +Getting an access token is the usual third step in the 3-legged OAuth1 flow. +This method retrieves an access token when provided `consumerKey`, `consumerSecret`, `oauthToken`, and `oauthTokenSecret`. +If you don't have a token for this call, you need to make a call to get the request token as described above. + +```csharp +var authenticator = OAuth1Authenticator.ForAccessToken( + consumerKey, consumerSecret, oauthToken, oauthTokenSecret +); +var options = new RestClientOptions("https://api.twitter.com") { + Authenticator = authenticator +}; +var client = new RestClient(options); +var request = new RestRequest("oauth/access_token"); +``` + +If the second step in 3-leg OAuth1 flow returned a verifier value, you can use another overload of `ForAccessToken`: + +```csharp +var authenticator = OAuth1Authenticator.ForAccessToken( + consumerKey, consumerSecret, oauthToken, oauthTokenSecret, verifier +); +``` + +The response should contain the access token that can be used to make calls to protected resources. + +For refreshing access tokens, use one of the two overloads of `ForAccessToken` that accept `sessionHandle`. + +### Protected resource + +When the access token is available, use `ForProtectedResource` function to get the authenticator for accessing protected resources. + +```csharp +var authenticator = OAuth1Authenticator.ForAccessToken( + consumerKey, consumerSecret, accessToken, accessTokenSecret +); +var options = new RestClientOptions("https://api.twitter.com/1.1") { + Authenticator = authenticator +}; +var client = new RestClient(options); +var request = new RestRequest("statuses/update.json", Method.Post) + .AddParameter("status", "Hello Ladies + Gentlemen, a signed OAuth request!") + .AddParameter("include_entities", "true"); +``` + +### xAuth + +xAuth is a simplified version of OAuth1. It allows sending the username and password as `x_auth_username` and `x_auth_password` request parameters and directly get the access token. xAuth is not widely supported, but RestSharp still allows using it. + +Create an xAuth authenticator using `OAuth1Authenticator.ForClientAuthentication` function: + +```csharp +var authenticator = OAuth1Authenticator.ForClientAuthentication( + consumerKey, consumerSecret, username, password +); +``` + +### 0-legged OAuth + +The access token authenticator can be used in 0-legged OAuth scenarios by providing `null` for the `consumerSecret`. + +```csharp +var authenticator = OAuth1Authenticator.ForAccessToken( + consumerKey, null, oauthToken, oauthTokenSecret +); +``` + +## OAuth2 + +RestSharp has two very simple authenticators to send the access token as part of the request. + +`OAuth2UriQueryParameterAuthenticator` accepts the access token as the only constructor argument, and it will send the provided token as a query parameter `oauth_token`. + +`OAuth2AuthorizationRequestHeaderAuthenticator` has two constructors. One only accepts a single argument, which is the access token. The other constructor also allows you to specify the token type. The authenticator will then add an `Authorization` header using the specified token type or `OAuth` as the default token type, and the token itself. + +For example: + +```csharp +var authenticator = new OAuth2AuthorizationRequestHeaderAuthenticator( + token, "Bearer" +); +var options = new RestClientOptions("https://example.com") { + Authenticator = authenticator +}; +var client = new RestClient(options); +``` + +The code above will tell RestSharp to send the bearer token with each request as a header. Essentially, the code above does the same as the sample for `JwtAuthenticator` below. + +As those authenticators don't do much to get the token itself, you might be interested in looking at our [sample OAuth2 authenticator](../usage/example.md#authenticator), which requests the token on its own. + +## JWT + +The JWT authentication can be supported by using `JwtAuthenticator`. It is a very simple class that can be constructed like this: + +```csharp +var authenticator = new JwtAuthenticator(myToken); +var options = new RestClientOptions("https://example.com") { + Authenticator = authenticator +}; +var client = new RestClient(options); +``` + +For each request, it will add an `Authorization` header with the value `Bearer `. + +As you might need to refresh the token from, you can use the `SetBearerToken` method to update the token. + +## Custom authenticator + +You can write your own implementation by implementing `IAuthenticator` and +registering it with your RestClient: + +```csharp +var authenticator = new SuperAuthenticator(); // implements IAuthenticator +var options = new RestClientOptions("https://example.com") { + Authenticator = authenticator +}; +var client = new RestClient(options); +``` + +The `Authenticate` method is the very first thing called upon calling `RestClient.Execute` or `RestClient.Execute`. +It gets the `RestRequest` currently being executed giving you access to every part of the request data (headers, parameters, etc.) + +You can find an example of a custom authenticator that fetches and uses an OAuth2 bearer token [here](../usage/example.md#authenticator). diff --git a/docs/docs/advanced/configuration.md b/docs/docs/advanced/configuration.md new file mode 100644 index 000000000..b57638632 --- /dev/null +++ b/docs/docs/advanced/configuration.md @@ -0,0 +1,230 @@ +--- +title: Configuration +description: Learn how to configure RestClient for non-trivial use cases. +sidebar_position: 1 +--- + +# Configuring RestClient + +This page describes how to create and configure `RestClient`. + +## Basic configuration + +The primary `RestClient` constructor accepts an instance of `RestClientOptions`. Most of the time, default option values don't need to be changed. However, in some cases, you'd want to configure the client differently, so you'd need to change some of the options in your code. The constructor also contains a few optional parameters for additional configuration that is not covered by client options. Here's the constructor signature: + +```csharp +public RestClient( + RestClientOptions options, + ConfigureHeaders? configureDefaultHeaders = null, + ConfigureSerialization? configureSerialization = null, + bool useClientFactory = false +) +``` + +Constructor parameters are: + +| Name | Description | Mandatory | +|-------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------| +| options | Client options | Yes | +| configureDefaultHeaders | Function to configure headers. Allows to configure default headers for `HttpClient`. Most of the time you'd prefer using `client.AddDefaultHeader` instead. | No | +| configureSerialization | Function to configure client serializers with non-default options or to use a different serializer ([learn more](serialization.md)) | No | +| useClientFactory | Instructs the client to use `SimpleFactory` ([learn more](../usage/client.md#simple-factory)) to get an `HttpClient` instance | No | + +Here's an example of how to create a client using client options: + +```csharp +var options = new RestClientOptions("https://localhost:5000/api") { + DisableCharset = true +}; +var client = new RestClient(options); +``` + +When you only need to set the base URL, you can use a simplified constructor: + +```csharp +var client = new RestClient("https://localhost:5000/api"); +``` + +The simplified constructor will create an instance of client options and set the base URL provided as the constructor argument. + +Finally, you can override properties of default options using a configuration function. Here's the constructor signature that supports this method: + +```csharp +public RestClient( + ConfigureRestClient? configureRestClient = null, + ConfigureHeaders? configureDefaultHeaders = null, + ConfigureSerialization? configureSerialization = null, + bool useClientFactory = false +) +``` + +For example: + +```csharp +var client = new RestClient(options => { + options.BaseUrl = new Url("https://localhost:5000/api"), + options.DisableCharset = true +}); +``` + +You can also provide the base URL as a constructor argument like this: + +```csharp +var client = new RestClient("https://localhost:5000/api", options => { + options.DisableCharset = true +}); +``` + +## Using custom HttpClient + +By default, RestSharp creates an instance of `HttpClient` configured using the client options, and keeps it during the lifetime of the client. When the `RestClient` instance gets disposed, it also disposes the `HttpClient` instance. + +There might be a case when you need to provide your own `HttpClient`. For example, you would want to use `HttpClient` created by HTTP client factory. RestSharp allows you to do it by using additional constructors. These constructors are: + +```csharp +// Create a client using an existing HttpClient and RestClientOptions (optional) +public RestClient( + HttpClient httpClient, + RestClientOptions? options, + bool disposeHttpClient = false, + ConfigureSerialization? configureSerialization = null +) + +// Create a client using an existing HttpClient and optional RestClient configuration function +public RestClient( + HttpClient httpClient, + bool disposeHttpClient = false, + ConfigureRestClient? configureRestClient = null, + ConfigureSerialization? configureSerialization = null +) +``` + +The `disposeHttpClient` argument tells the client to dispose `HttpClient` when the client itself gets disposed. It's set to `false` by default as when the `HttpClient` is provided from the outside, it should normally be disposed on the outside as well. + +## Using custom message handler + +Unless you use an external instance of `HttpClient`, the `RestClient` creates one when being constructed, and it will use the default HTTP message handler, configured using `RestClientOptions`. Normally, you'd get a `SocketHttpHandler` with modern .NET, and `WinHttpHandler` with .NET Framework. + +There might be a case when you need to configure the HTTP message handler. For example, you want to add a delegating message handler. RestSharp allows you to do it by using additional constructors. There's one constructor that allows you to pass the custom `HttpMessageHandler`: + +```csharp +public RestClient( + HttpMessageHandler handler, + bool disposeHandler = true, + ConfigureRestClient? configureRestClient = null, + ConfigureSerialization? configureSerialization = null +) +``` + +This constructor will create a new `HttpClient` instance using the provided message handler. As RestSharp will dispose the `HttpClient` instance when the `RestClient` instance gets disposed, the handler will be disposed as well. If you want to change that and keep the handler, set the `disposeHandler` parameter to `false`. + +:::note +When using a custom message handler, RestSharp **will not** configure it with client options, which are normally used to configure the handler created by RestSharp. +::: + +Another way to customize the message handler is to allow RestSharp to create a handler, but then configure it, or wrap it in a delegating handler. It can be done by using the `RestClientOptions.ConfigureMessageHandler` property. It can be set to a function that receives the handler created by RestSharp and returned either the same handler with different settings, or a new handler. + +For example, if you want to use `MockHttp` and its handler for testing, you can do it like this: + +```csharp +var mockHttp = new MockHttpMessageHandler(); +// Configure the MockHttp handler to do the checks +... + +var options = new RestClientOptions(Url) { + ConfigureMessageHandler = _ => mockHttp +}; +using var client = new RestClient(options); +``` + +In this example, we are reassigning the handler to MockHttp, so the handler created by RestSharp isn't used. In other cases you want to use delegating handlers as middleware, so you'd pass the handler created by RestSharp to the delegating handler: + +```csharp +var options = new RestClientOptions(Url) { + ConfigureMessageHandler = handler => new MyDelegatingHandler(handler) +}; +using var client = new RestClient(options); +``` + +## Client options + +RestSharp allows configuring `RestClient` using client options, as mentioned at the beginning of this page. Below, you find more details about available options. + +| Option | Description | +|----------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `BaseUrl` | Client base URL. It can also be provided as the `RestClientOptions` constructor argument. | +| `ConfigureMessageHandler` | Configures the HTTP message handler (see above). | +| `CalculateResponseStatus` | Function to calculate a different response status from `HttpResponseMessage`. By default, the request is considered as complete if it returns a successful status code or 404. | +| `Authenticator` | Client-level authenticator. Read more about authenticators [here](authenticators.md). | +| `Interceptors` | A collector of interceptors. Read more about interceptors [here](interceptors.md). | +| `Credentials` | Instance of `ICredentials` used for NTLM or Kerberos authentication. Not supported in browsers. | +| `UseDefaultCredentials` | Whether to use default OS credentials for NTLM or Kerberos authentication. Not supported in browsers. | +| `DisableCharset` | When set to `true`, the `Content-Type` header won't have the `charset` portion. Some older web servers don't understand the `charset` portion in the header and fail to process the request. | +| `AutomaticDecompression` | Allows customizing supported decompression methods. Default is `All` except for .NET Framework that only support `GZip`. Not supported in browsers. | +| `MaxRedirects` | The number of redirects to follow. Not supported in browsers. | +| `ClientCertificates` | A collection of X.509 client certificates to be used for authentication. Not supported in browsers. | +| `Proxy` | Can be used if the client needs to use an explicit, non-default proxy. Not supported in browsers, on iOS and tvOS. | +| `CachePolicy` | Shortcut for setting the default value for `Cache-Control` header. | +| `FollowRedirects` | Instructs the client to follow redirects. Default is `true`. | +| `Expect100Continue` | Gets or sets a value that indicates if the `Expect` header for an HTTP request contains `Continue`. | +| `UserAgent` | Allows overriding the default value for `User-Agent` header, which is `RestSharp/{version}`. | +| `PreAuthenticate` | Gets or sets a value that indicates whether the client sends an `Authorization` header with the request. Not supported in browsers. | +| `RemoteCertificateValidationCallback` | Custom function to validate the server certificate. Normally, it's used when the server uses a certificate that isn't trusted by default. | +| `BaseHost` | Value for the `Host` header sent with each request. | +| `CookieContainer` | Custom cookie container that will be shared among all calls made by the client. Normally not required as RestSharp handles cookies without using a client-level cookie container. | +| `MaxTimeout` | Client-level timeout in milliseconds. If the request timeout is also set, this value isn't used. | +| `Encoding` | Default request encoding. Override it only if you don't use UTF-8. | +| `ThrowOnDeserializationError` | Forces the client to throw if it fails to deserialize the response. Remember that not all deserialization issues forces the serializer to throw. Default is `false`, so the client will return a `RestResponse` with deserialization exception details. Only relevant for `Execute...` functions. | +| `FailOnDeserializationError` | When set to `true`, if the client fails to deserialize the response, the response object will have status `Failed`, although the HTTP calls might have been successful. Default is `true`. | +| `ThrowOnAnyError` | When set to `true`, the client will re-throw any exception from `HttpClient`. Default is `false`. Only applies for `Execute...` functions. | +| `AllowMultipleDefaultParametersWithSameName` | By default, adding parameters with the same name is not allowed. You can override this behaviour by setting this property to `true`. | +| `Encode` | A function to encode URLs, the default is a custom RestSharp function based on `Uri.EscapeDataString()`. Set it if you need a different way to do the encoding. | +| `EncodeQuery` | A function to encode URL query parameters. The default is the same function as for `Encode` property. | + +Some of the options are used by RestSharp code, but some are only used to configure the `HttpMessageHandler`. These options are: +- `Credentials` +- `UseDefaultCredentials` +- `AutomaticDecompression` +- `PreAuthenticate` +- `MaxRedirects` +- `RemoteCertificateValidationCallback` +- `ClientCertificates` +- `FollowRedirects` +- `Proxy` + +:::note +If setting these options to non-default values produce no desirable effect, check if your framework and platform supports them. RestSharp doesn't change behaviour based on values of those options. +::: + +The `IRestClient` interface exposes the `Options` property, so any option can be inspected at runtime. However, RestSharp converts the options object provided to the client constructor to an immutable object. Therefore, no client option can be changed after the client is instantiated. It's because changing client options at runtime can produce issues in concurrent environments, effectively rendering the client as not thread-safe. Apart from that, changing the options that are used to create the message handler would require re-creating the handler, and also `HttpClient`, which should not be done at runtime. + +## Configuring requests + +Client options apply to all requests made by the client. Sometimes, you want to fine-tune particular requests, so they execute with custom configuration. It's possible to do using properties of `RestRequest`, described below. + +| Name | Description | +|------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `AlwaysMultipartFormData` | When set to `true`, the request will be sent as a multipart form, even though it's not required. By default, RestSharp only sends requests with multiple attachments as multipart forms. Default is `false`. | +| `AlwaysSingleFileAsContent` | When set to true, the request with file attachment will not be sent as a multipart form, but as plain content. Default is `false`. It cannot be set to `true` when `AlwaysMultipartFormData` is set to `true`, or when the request has `POST` parameters. | +| `MultipartFormQuoteBoundary` | Default is `true`, which means that the form boundary string will be wrapped in quotes. If the server has an issue with that, setting this to `false` will remove quotes around the boundary. | +| `FormBoundary` | Allows specifying a custom multipart form boundary instead of using the default random string. | +| `RequestParameters` | Collection of request parameters. Normally, you won't need to use it as parameters are added to the request using `Add...` functions. | +| `CookieContainer` | Custom request-level cookie container. Default is `null`. You can still set request cookies using `AddCookie` and get response cookies from the response object without using cooking container. | +| `Authenticator` | Overrides the client-level authenticator. | +| `Files` | Collection of file parameters, read-only. Use `AddFile` for adding files to the request. | +| `Method` | Request HTTP method, default is `GET`. Only needed when using `Execute` or `ExecuteAsync` as other functions like `ExecutePostAsync` will override the request method. | +| `TImeout` | Overrides the client-level timeout. | +| `Resource` | Resource part of the remote endpoint URL. For example, when using the client-level base URL `https://localhost:5000/api` and `Resource` set to `weather`, the request will be sent to `https://localhost:5000/api/weather`. It can container resource placeholders to be used in combination with `AddUrlSegment` | +| `RequestFormat` | Identifies the request as JSON, XML, binary, or none. Rarely used because the client will set the request format based on the body type if functions like `AddJsonBody` or `AddXmlBody` are used. | +| `RootElement` | Used by the default deserializers to determine where to start deserializing from. Only supported for XML responses. Does not apply to requests. | +| `OnBeforeDeserialization` | **Obsolete** A function to be called before the response is deserializer. Allows changing the content before calling the deserializer. Use [interceptors](interceptors.md) instead. | +| `OnBeforeRequest` | **Obsolete** A function to be called right before the request is executed by `HttpClient`. It receives an instance of `HttpRequestMessage`. Use [interceptors](interceptors.md) instead. | +| `OnAfterRequest` | **Obsolete** A function to be called right after the request is executed by `HttpClient`. It receives an instance of `HttpResponseMessage`. Use [interceptors](interceptors.md) instead. | +| `Attempts` | When the request is being resent to retry, the property value increases by one. | +| `CompletionOption` | Instructs the client on when it should consider the request to be completed. The default is `ResponseContentRead`. It is automatically changed to `ResponseHeadersRead` when using async download functions or streaming. | +| `CachePolicy` | Overrides the client cache policy. | +| `ResponseWriter` | Allows custom handling of the response stream. The function gets the raw response stream and returns another stream or `null`. Cannot be used in combination with `AdvancedResponseWriter`. | +| `AdvancedResponseWriter` | Allows custom handling of the response. The function gets an instance of `HttpResponseMessage` and an instance of `RestRequest`. It must return an instance of `RestResponse`, so it effectively overrides RestSharp default functionality for creating responses. | +| `Interceptors` | Allows adding interceptors to the request. Both client-level and request-level interceptors will be called. | + +The table below contains all configuration properties of `RestRequest`. To learn more about adding request parameters, check the [usage page](../usage/request.md) page about creating requests with parameters. diff --git a/docs/docs/advanced/error-handling.md b/docs/docs/advanced/error-handling.md new file mode 100644 index 000000000..80e822dcc --- /dev/null +++ b/docs/docs/advanced/error-handling.md @@ -0,0 +1,71 @@ +# Error handling + +If there is a network transport error (network is down, failed DNS lookup, etc.), or any kind of server error (except 404), `RestResponse.ResponseStatus` will be set to `ResponseStatus.Error`, otherwise it will be `ResponseStatus.Completed`. + +If an API returns a 404, `ResponseStatus` will still be `Completed`. If you need access to the HTTP status code returned, you will find it at `RestResponse.StatusCode`. +The `Status` property is an indicator of completion independent of the API error handling. + +Normally, RestSharp doesn't throw an exception if the request fails. + +However, it is possible to configure RestSharp to throw in different situations when it normally doesn't throw +in favor of giving you the error as a property. + +| Property | Behavior | +|-------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `FailOnDeserializationError` | Changes the default behavior when failed deserialization results in a successful response with an empty `Data` property of the response. Setting this property to `true` will tell RestSharp to consider failed deserialization as an error and set the `ResponseStatus` to `Error` accordingly. | +| `ThrowOnDeserializationError` | Changes the default behavior when failed deserialization results in empty `Data` property of the response. Setting this property to `true` will tell RestSharp to throw when deserialization fails. | +| `ThrowOnAnyError` | Setting this property to `true` changes the default behavior and forces RestSharp to throw if any errors occurs when making a request or during deserialization. | + +Those properties are available for the `RestClientOptions` and will be used for all request made with the client instance. + +For example, you can configure the client to throw an exception if any error occurs when making a request or when a request returns a non-successful HTTP status code: + +```csharp +var options = new RestClientOptions(url) { + ThrowOnAnyError = true +}; +var client = new RestClient(options); +var request = new RestRequest("resource/{id}").AddUrlSegment("id", 123); + +// 👇 will throw if the request fails +var deserialized = await client.GetAsync(request); + +// 👇 will NOT throw if the request fails, inspect the response to find out what happened +var response = await client.ExecuteGetAsync(request); +``` + +:::warning +Please be aware that deserialization failures will only work if the serializer throws an exception when deserializing the response. +Many serializers don't throw by default, and just return a `null` result. RestSharp is unable to figure out why `null` is returned, so it won't fail in this case. +Check the serializer documentation to find out if it can be configured to throw on deserialization error. +::: + +There are also slight differences on how different overloads handle exceptions. + +Asynchronous generic methods `GetAsync`, `PostAsync` and so on, which aren't a part of `RestClient` API (those methods are extension methods) return `Task`. It means that there's no `RestResponse` to set the response status to error. We decided to throw an exception when such a request fails. It is a trade-off between the API consistency and usability of the library. Usually, you only need the content of `RestResponse` instance to diagnose issues and most of the time the exception would tell you what's wrong. + +Below, you can find how different extensions deal with errors. Note that functions, which don't throw by default, will throw exceptions when `ThrowOnAnyError` is set to `true`. + +| Function | Throws on errors | +|:----------------------|:-----------------| +| `ExecuteAsync` | No | +| `ExecuteGetAsync` | No | +| `ExecuteGetAsync` | No | +| `ExecutePostAsync` | No | +| `ExecutePostAsync` | No | +| `ExecutePutAsync` | No | +| `ExecutePutAsync` | No | +| `GetAsync` | Yes | +| `GetAsync` | Yes | +| `PostAsync` | Yes | +| `PostAsync` | Yes | +| `PatchAsync` | Yes | +| `PatchAsync` | Yes | +| `DeleteAsync` | Yes | +| `DeleteAsync` | Yes | +| `OptionsAsync` | Yes | +| `OptionsAsync` | Yes | +| `HeadAsync` | Yes | +| `HeadAsync` | Yes | + +In addition, all the functions for JSON requests, like `GetJsonAsync` and `PostJsonAsync` throw an exception if the HTTP call fails. diff --git a/docs/docs/advanced/interceptors.md b/docs/docs/advanced/interceptors.md new file mode 100644 index 000000000..a3bf7a9bc --- /dev/null +++ b/docs/docs/advanced/interceptors.md @@ -0,0 +1,84 @@ +--- +title: Interceptors +--- + +## Intercepting requests and responses + +Interceptors are a powerful feature of RestSharp that allows you to modify requests and responses before they are sent or received. You can use interceptors to add headers, modify the request body, or even cancel the request. You can also use interceptors to modify the response before it is returned to the caller. + +### Implementing an interceptor + +To implement an interceptor, you need to create a class that inherits the `Interceptor` base class. The base class implements all interceptor methods as virtual, so you can override them in your derived class. + +Methods that you can override are: +- `BeforeRequest(RestRequest request, CancellationToken cancellationToken)` +- `AfterRequest(RestResponse response, CancellationToken cancellationToken)` +- `BeforeHttpRequest(HttpRequestMessage requestMessage, CancellationToken cancellationToken)` +- `AfterHttpRequest(HttpResponseMessage responseMessage, CancellationToken cancellationToken)` +- `BeforeDeserialization(RestResponse response, CancellationToken cancellationToken)` + +All those functions must return a `ValueTask` instance. + +Here's an example of an interceptor that adds a header to a request: + +```csharp +// This interceptor adds a header to the request +// You'd not normally use this interceptor, as RestSharp already has a method +// to add headers to the request +class HeaderInterceptor(string headerName, string headerValue) : Interceptors.Interceptor { + public override ValueTask BeforeHttpRequest(HttpRequestMessage requestMessage, CancellationToken cancellationToken) { + requestMessage.Headers.Add(headerName, headerValue); + return ValueTask.CompletedTask; + } +} +``` + +Because interceptor functions return `ValueTask`, you can use `async` and `await` inside them. + +### Using an interceptor + +It's possible to add as many interceptors as you want, both to the client and to the request. The interceptors are executed in the order they were added. + +Adding interceptors to the client is done via the client options: + +```csharp +var options = new RestClientOptions("https://api.example.com") { + Interceptors = [new HeaderInterceptor("Authorization", token)] +}; +var client = new RestClient(options); +``` + +When you add an interceptor to the client, it will be executed for every request made by that client. + +You can also add an interceptor to a specific request: + +```csharp +var request = new RestRequest("resource") { + Interceptors = [new HeaderInterceptor("Authorization", token)] +}; +``` + +In this case, the interceptor will only be executed for that specific request. + +### Deprecation notice + +Interceptors aim to replace the existing request hooks available in RestSharp prior to version 111.0. Those hooks are marked with `Obsolete` attribute and will be removed in the future. If you are using those hooks, we recommend migrating to interceptors as soon as possible. + +To make the migration easier, RestSharp provides a class called `CompatibilityInterceptor`. It has properties for the hooks available in RestSharp 110.0 and earlier. You can use it to migrate your code to interceptors without changing the existing logic. + +For example, a code that uses `OnBeforeRequest` hook: + +```csharp +var request = new RestRequest("success"); +request.OnBeforeDeserialization += _ => throw new Exception(exceptionMessage); +``` + +Can be migrated to interceptors like this: + +```csharp +var request = new RestRequest("success") { + Interceptors = [new CompatibilityInterceptor { + OnBeforeDeserialization = _ => throw new Exception(exceptionMessage) + }] +}; +``` \ No newline at end of file diff --git a/docs/docs/advanced/serialization.md b/docs/docs/advanced/serialization.md new file mode 100644 index 000000000..b7092519a --- /dev/null +++ b/docs/docs/advanced/serialization.md @@ -0,0 +1,148 @@ +# Serialization + +One of the most common reasons to choose RestSharp over plain `HttpClient` is its rich build-in serialization support. RestSharp allows adding complex objects as request body to be serialized when making a call to an API endpoint, and deserializing the response to a given .NET type. RestSharp supports JSON and XML serialization and deserialization by default. In addition, you can use a CSV serializer or write your own. + +In contrast to `System.Net.Http.Json` package that contains `HttpClient` extensions to make `GET` or `POST` calls using JSON, RestSharp support JSON responses for all HTTP methods, not just for `GET`. + +## Configuration + +:::tip +The default behavior of RestSharp is to swallow deserialization errors and return `null` in the `Data` +property of the response. Read more about it in the [Error Handling](error-handling.md). +::: + +You can tell RestSharp to use a custom serializer by using the `configureSerialization` constructor parameter: + +```csharp +var client = new RestClient( + options, + configureSerialization: s => s.UseSerializer(() => new CustomSerializer()); +); +``` + +All RestSharp serializers implement the `IRestSerializer` interface. Among other things, the interface requires implementing the `AcceptedContentTypes` property, which must return a collection of content types supported by the serializer. Being configured to use certain serializers, RestSharp populates the `Accept` header accordingly, so it doesn't need to be set manually. + +When making a call, RestSharp sets the request content type according to the request body type. For example, when you use `AddJsonBody`, the content type is set to `application/json`. Normally, you won't need to set the `Content-Type` header manually. If you need to set a custom content type for a JSON call, you can use the optional `contentType` argument of `AddJsonBody`, for example: + +```csharp +request.AddJsonBody(data, "text/json"); +``` + +## JSON + +The default JSON serializer uses `System.Text.Json`, which is a part of .NET since .NET 6. For earlier versions, it is added as a dependency. There are also a few serializers provided as additional packages. + +By default, RestSharp will use `JsonSerializerDefaults.Web` configuration. If necessary, you can specify your own options: + +```csharp +var client = new RestClient( + options, + configureSerialization: s => s.UseSystemTextJson(new JsonSerializerOptions {...}) +); +``` + +## XML + +The default XML serializer is `DotNetXmlSerializer`, which uses `System.Xml.Serialization` library from .NET. + +In previous versions of RestSharp, the default XML serializer was a custom RestSharp XML serializer. To make the code library size smaller, that serializer is now available as a separate package [`RestSharp.Serializers.Xml`](https://www.nuget.org/packages/RestSharp.Serializers.Xml). +You can add it back if necessary by installing the package and adding it to the client: + +```csharp +var client = new RestClient( + options, + configureSerialization: s => s.UseXmlSerializer() +); +``` + +As before, you can supply three optional arguments for a custom namespace, custom root element, and if you want to use `SerializeAs` and `DeserializeAs` attributed. + +## NewtonsoftJson (aka Json.Net) + +The `NewtonsoftJson` package is the most popular JSON serializer for .NET. It handles all possible scenarios and is very configurable. Such a flexibility comes with the cost of performance. If you need speed, keep the default JSON serializer. + +RestSharp support Json.Net serializer via a separate package [`RestSharp.Serializers.NewtonsoftJson`](https://www.nuget.org/packages/RestSharp.Serializers.NewtonsoftJson). + +:::warning +Please note that `RestSharp.Newtonsoft.Json` package is not provided by RestSharp, is marked as obsolete on NuGet, and no longer supported by its creator. +::: + +Use the extension method provided by the package to configure the client: + +```csharp +var client = new RestClient( + options, + configureSerialization: s => s.UseNewtonsoftJson() +); +``` + +The serializer configures some options by default: + +```csharp +JsonSerializerSettings DefaultSettings = new JsonSerializerSettings { + ContractResolver = new CamelCasePropertyNamesContractResolver(), + DefaultValueHandling = DefaultValueHandling.Include, + TypeNameHandling = TypeNameHandling.None, + NullValueHandling = NullValueHandling.Ignore, + Formatting = Formatting.None, + ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor +}; +``` + +If you need to use different settings, you can supply your instance of +`JsonSerializerSettings` as a parameter for the extension method. + +## CSV + +A separate package `RestSharp.Serializers.CsvHelper` provides a CSV serializer for RestSharp. It is based on the +`CsvHelper` library. + +Use the extension method provided by the package to configure the client: + +```csharp +var client = new RestClient( + options, + configureSerialization: s => s.UseCsvHelper() +); +``` + +You can also supply your instance of `CsvConfiguration` as a parameter for the extension method. + +```csharp +var client = new RestClient( + options, + configureSerialization: s => s.UseCsvHelper( + new CsvConfiguration(CultureInfo.InvariantCulture) {...} + ) +); +``` + +## Custom + +You can also implement your custom serializer. To support both serialization and +deserialization, you must implement the `IRestSerializer` interface. + +Here is an example of a custom serializer that uses `System.Text.Json`: + +```csharp +public class SimpleJsonSerializer : IRestSerializer { + public string? Serialize(object? obj) => obj == null ? null : JsonSerializer.Serialize(obj); + + public string? Serialize(Parameter bodyParameter) => Serialize(bodyParameter.Value); + + public T? Deserialize(RestResponse response) => JsonSerializer.Deserialize(response.Content!); + + public ContentType ContentType { get; set; } = ContentType.Json; + + public ISerializer Serializer => this; + public IDeserializer Deserializer => this; + public DataFormat DataFormat => DataFormat.Json; + public string[] AcceptedContentTypes => ContentType.JsonAccept; + public SupportsContentType SupportsContentType + => contentType => contentType.Value.EndsWith("json", StringComparison.InvariantCultureIgnoreCase); +} +``` + +The `SupportedContentTypes` function will be used to check if the serializer is able to deserialize the response based on the `Content-Type` response header. + +The `ContentType` property will be used when making a request so the server knows how to handle the payload. diff --git a/docs/docs/changelog.md b/docs/docs/changelog.md new file mode 100644 index 000000000..f637a8f51 --- /dev/null +++ b/docs/docs/changelog.md @@ -0,0 +1,19 @@ +--- +title: What's new +description: List of changes for the current major version +sidebar_position: 1 +--- + +# Changelog + +For release notes of previous versions, please check the [Releases page](https://github.com/restsharp/RestSharp/releases) in RestSharp GitHub repository. + +Changes between major versions are documented in the documentation for each version on this website. + +# v112.0 + +* Security fix for [CVE-2024-45302](https://github.com/restsharp/RestSharp/security/advisories/GHSA-4rr6-2v9v-wcpc). Header values cannot contain `CRLF`. + +## v112.1 + +* Follow up on v112.0 security fix: remove `\t` from the list of forbidden characters in headers. diff --git a/docs/docs/intro.md b/docs/docs/intro.md new file mode 100644 index 000000000..961c5350a --- /dev/null +++ b/docs/docs/intro.md @@ -0,0 +1,112 @@ +--- +sidebar_position: 2 +title: Quick start +--- + +## Introduction + +:::warning +RestSharp v107+ changes the library API surface and its behaviour significantly. We advise looking at [migration](/migration) docs to understand how to migrate to the latest version of RestSharp. +::: + +The main purpose of RestSharp is to make synchronous and asynchronous calls to remote resources over HTTP. As the name suggests, the main audience of RestSharp are developers who use REST APIs. However, RestSharp can call any API over HTTP, as long as you have the resource URI and request parameters that you want to send comply with W3C HTTP standards. + +One of the main challenges of using HTTP APIs for .NET developers is to work with requests and responses of different kinds and translate them to complex C# types. RestSharp can take care of serializing the request body to JSON or XML and deserialize the response. It can also form a valid request URI based on different parameter kinds: path, query, form or body. + +## Getting Started + +Before you can use RestSharp in your application, you need to add the NuGet package. You can do it using your IDE or the command line: + +``` +dotnet add package RestSharp +``` + +### Basic Usage + +If you only have a small number of one-off API requests to perform, you can use RestSharp like this: + +```csharp +using RestSharp; +using RestSharp.Authenticators; + +var options = new RestClientOptions("https://api.twitter.com/1.1") { + Authenticator = new HttpBasicAuthenticator("username", "password") +}; +var client = new RestClient(options); +var request = new RestRequest("statuses/home_timeline.json"); +// The cancellation token comes from the caller. You can still make a call without it. +var response = await client.GetAsync(request, cancellationToken); +``` + +It will return a `RestResponse` back, which contains all the information returned from the remote server. +You have access to the headers, content, HTTP status and more. + +You can also use generic overloads like `Get` to automatically deserialize the response into a .NET class. + +For example: + +```csharp +using RestSharp; +using RestSharp.Authenticators; + +var options = new RestClientOptions("https://api.twitter.com/1.1") { + Authenticator = new HttpBasicAuthenticator("username", "password") +}; +var client = new RestClient(options); + +var request = new RestRequest("statuses/home_timeline.json"); + +// The cancellation token comes from the caller. You can still make a call without it. +var timeline = await client.GetAsync(request, cancellationToken); +``` + +Both snippets above use the `GetAsync` extension, which is a wrapper about `ExecuteGetAsync`, which, in turn, is a wrapper around `ExecuteAsync`. +All `ExecuteAsync` overloads and return the `RestResponse` or `RestResponse`. + +The most important difference is that async methods named after HTTP methods (like `GetAsync` or `PostAsync`) return `Task` instead of `Task>`. It means that you won't get an error response if the request fails as those methods throw an exception for unsuccessful HTTP calls. For keeping the API consistent, non-generic functions like `GetAsync` or `PostAsync` also throw an exception if the request fails, although they return the `Task`. + +Read [here](advanced/error-handling.md) about how RestSharp handles exceptions. + +RestSharp also offers simple ways to call APIs that accept and return JSON payloads. You can use the `GetJsonAsync` and `PostJsonAsync` extension methods, which will automatically serialize the request body to JSON and deserialize the response to the specified type. + +```csharp +var client = new RestClient(options); +var timeline = await client.GetJsonAsync("statuses/home_timeline.json", cancellationToken); +``` + +Read [here](usage/execute.md#json-requests) about making JSON calls without preparing a request object. + +### Content type + +RestSharp supports sending XML or JSON body as part of the request. To add a body to the request, simply call `AddJsonBody` or `AddXmlBody` method of the `RestRequest` object. + +There is no need to set the `Content-Type` or add the `DataFormat` parameter to the request when using those methods, RestSharp will do it for you. + +RestSharp will also handle both XML and JSON responses and perform all necessary deserialization tasks, depending on the server response type. Therefore, you only need to add the `Accept` header if you want to deserialize the response manually. + +For example, only you'd only need these lines to make a request with JSON body: + +```csharp +var request = new RestRequest("address/update").AddJsonBody(updatedAddress); +var response = await client.PostAsync(request); +``` + +It's also possible to make the same call using `PostAsync` shorter syntax: + +```csharp +var response = await PostJsonAsync( + "address/update", request, cancellationToken +); +``` + +Read more about serialization and deserialization [here](advanced/serialization.md). + +### Response + +When you use `ExecuteAsync`, you get an instance of `RestResponse` back. The response object has the `Content` property, which contains the response as string. You can find other useful properties there, like `StatusCode`, `ContentType` and so on. If the request wasn't successful, you'd get a response back with `IsSuccessful` property set to `false` and the error explained in the `ErrorException` and `ErrorMessage` properties. + +When using typed `ExecuteAsync`, you get an instance of `RestResponse` back, which is identical to `RestResponse` but also contains the `T Data` property with the deserialized response. + +None of `ExecuteAsync` overloads throw if the remote server returns an error. You can inspect the response and find the status code, error message, and, potentially, an exception. + +Extensions like `GetAsync` will not return the whole `RestResponse` but just a deserialized response. These extensions will throw an exception if the remote server returns an error. The exception details contain the status code returned by the server. diff --git a/docs/docs/usage/_category_.json b/docs/docs/usage/_category_.json new file mode 100644 index 000000000..bd2045cd6 --- /dev/null +++ b/docs/docs/usage/_category_.json @@ -0,0 +1,7 @@ +{ + "label": "Using RestSharp", + "position": 3, + "link": { + "type": "generated-index" + } +} diff --git a/docs/docs/usage/basics.md b/docs/docs/usage/basics.md new file mode 100644 index 000000000..6bbb5594a --- /dev/null +++ b/docs/docs/usage/basics.md @@ -0,0 +1,23 @@ +--- +sidebar_position: 2 +--- + +# RestSharp basics + +This page describes some of the essential properties and features of RestSharp. + +## What RestSharp does + +Essentially, RestSharp is a wrapper around `HttpClient` that allows you to do the following: +- Add default parameters of any kind (not just headers) to the client, once +- Add parameters of any kind to each request (query, URL segment, form, attachment, serialized body, header) in a straightforward way +- Serialize the payload to JSON or XML if necessary +- Set the correct content headers (content type, disposition, length, etc.) +- Handle the remote endpoint response +- Deserialize the response from JSON or XML if necessary + +## API client + +The best way to call an external HTTP API is to create a typed client, which encapsulates RestSharp calls and doesn't expose the `RestClient` instance in public. + +You can find an example of a Twitter API client on the [Example](example.md) page. diff --git a/docs/docs/usage/client.md b/docs/docs/usage/client.md new file mode 100644 index 000000000..e84b955f4 --- /dev/null +++ b/docs/docs/usage/client.md @@ -0,0 +1,115 @@ +--- +sidebar_position: 3 +title: Creating the client +--- + +## Constructors + +A RestSharp client can be instantiated by one of its constructors. Two most commonly used constructors are: + +#### Only specify the base URL + +You can create an instance of `RestClient` with only a single parameter: the base URL. Even that isn't required as base URL can be left empty. In that case, you'd need to specify the absolute path for each call. When the base URL is set, you can use both relative and absolute path. + +```csharp +// Creates a client with default options to call a given base URL +var client = new RestClient("https://localhost:5000"); +``` + +#### Provide client options + +The most common way to create a client is to use the constructor with options. The options object has the type of `RestClientOptions`. +Here's an example of how to create a client using the same base path as in the previous sample, but with a couple additional settings: + +```csharp +// Creates a client using the options object +var options = new RestClientOptions("https://localhost:5000") { + MaxTimeout = 1000 +}; +var client = new RestClient(options); +``` + +#### Advanced configuration + +RestSharp can be configured with more tweaks, including default request options, how it should handle responses, how serialization works, etc. You can also provide your own instance of `HttpClient` or `HttpMessageHandler`. + +Read more about the advanced configuration of RestSharp on a [dedicated page](../advanced/configuration.md). + +## Simple factory + +Another way to create the client instance is to use a simple client factory. The factory will use the `BaseUrl` property of the client options to cache `HttpClient` instances. Every distinct base URL will get its own `HttpClient` instance. Other options don't affect the caching. Therefore, if you use different options for the same base URL, you'll get the same `HttpClient` instance, which will not be configured with the new options. Options that aren't applied _after_ the first client instance is created are: + +* `Credentials` +* `UseDefaultCredentials` +* `AutomaticDecompression` +* `PreAuthenticate` +* `FollowRedirects` +* `RemoteCertificateValidationCallback` +* `ClientCertificates` +* `MaxRedirects` +* `MaxTimeout` +* `UserAgent` +* `Expect100Continue` + +Constructor parameters to configure the `HttpMessageHandler` and default `HttpClient` headers configuration are also ignored for the cached instance as the factory only configures the handler once. + +You need to set the `useClientFactory` parameter to `true` in the `RestClient` constructor to enable the factory. + +```csharp +var options = new RestClientOptions("https://api.twitter.com/2"); +var client = new RestClient(options, useClientFactory: true); +``` + +## Reusing HttpClient + +RestSharp uses `HttpClient` internally to make HTTP requests. It's possible to reuse the same `HttpClient` instance for multiple `RestClient` instances. This is useful when you want to share the same connection pool between multiple `RestClient` instances. + +One way of doing it is to use `RestClient` constructors that accept an instance of `HttpClient` or `HttpMessageHandler` as an argument. Note that in that case not all the options provided via `RestClientOptions` will be used. Here is the list of options that will work: + +- `BaseAddress` is be used to set the base address of the `HttpClient` instance if base address is not set there already. +- `MaxTimeout` is used to cancel the call using the cancellation token source, so +- `UserAgent` will be added to the `RestClient.DefaultParameters` list as a HTTP header. This will be added to each request made by the `RestClient`, and the `HttpClient` instance will not be modified. This is to allow the `HttpClient` instance to be reused for scenarios where different `User-Agent` headers are required. +- `Expect100Continue` + +Another option is to use a simple HTTP client factory as described [above](#simple-factory). + +## Blazor support + +Inside a Blazor webassembly app, you can make requests to external API endpoints. Microsoft examples show how to do it with `HttpClient`, and it's also possible to use RestSharp for the same purpose. + +You need to remember that webassembly has some platform-specific limitations. Therefore, you won't be able to instantiate `RestClient` using all of its constructors. In fact, you can only use `RestClient` constructors that accept `HttpClient` or `HttpMessageHandler` as an argument. If you use the default parameterless constructor, it will call the option-based constructor with default options. The options-based constructor will attempt to create an `HttpMessageHandler` instance using the options provided, and it will fail with Blazor, as some of those options throw thw "Unsupported platform" exception. + +Here is an example how to register the `RestClient` instance globally as a singleton: + +```csharp +builder.Services.AddSingleton(new RestClient(new HttpClient())); +``` + +Then, on a page you can inject the instance: + +```html +@page "/fetchdata" +@using RestSharp +@inject RestClient _restClient +``` + +And then use it: + +```csharp +@code { + private WeatherForecast[]? forecasts; + + protected override async Task OnInitializedAsync() { + forecasts = await _restClient.GetJsonAsync("http://localhost:5104/weather"); + } + + public class WeatherForecast { + public DateTime Date { get; set; } + public int TemperatureC { get; set; } + public string? Summary { get; set; } + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + } +} +``` + +In this case, the call will be made to a WebAPI server hosted at `http://localhost:5104/weather`. Remember that if the WebAPI server is not hosting the webassembly itself, it needs to have a CORS policy configured to allow the webassembly origin to access the API endpoint from the browser. diff --git a/docs/docs/usage/example.md b/docs/docs/usage/example.md new file mode 100644 index 000000000..6182d8ff3 --- /dev/null +++ b/docs/docs/usage/example.md @@ -0,0 +1,152 @@ +--- +sidebar_position: 1 +--- + +# Example + +RestSharp works best as the foundation for a proxy class for your API. Each API would most probably require different settings for `RestClient`. Hence, a dedicated API class (and its interface) gives you sound isolation between different `RestClient` instances and make them testable. + +For example, let's look at a simple Twitter API v2 client, which uses OAuth2 machine-to-machine authentication. For it to work, you would need to have access to the Twitter Developers portal, a project, and an approved application inside the project with OAuth2 enabled. + +## Client model + +Before implementing an API client, we need to have a model for it. The model includes an abstraction for the client, which has functions for the API calls we are interested to implement. In addition, the client model would include the necessary request and response models. Usually those are simple classes or records without logic, which are often referred to as DTOs (data transfer objects). + +This example starts with a single function that retrieves one Twitter user. Lets being by defining the API client interface: + +```csharp +public interface ITwitterClient { + Task GetUser(string user); +} +``` + +As the function returns a `TwitterUser` instance, we need to define it as a model: + +```csharp +public record TwitterUser(string Id, string Name, string Username); +``` + +## Client implementation + +When that is done, we can implement the interface and add all the necessary code blocks to get a working API client. + +The client class needs the following: +- A constructor for passing API credentials +- A wrapped `RestClient` instance with the Twitter API base URI pre-configured +- An authenticator to support authorizing the client using Twitter OAuth2 authentication +- The actual function to get the user (to implement the `ITwitterClient` interface) + +Creating an authenticator is described [below](#authenticator). + +Here's how the client implementation could look like: + +```csharp +public class TwitterClient : ITwitterClient, IDisposable { + readonly RestClient _client; + + public TwitterClient(string apiKey, string apiKeySecret) { + var options = new RestClientOptions("https://api.twitter.com/2"); + _client = new RestClient(options); + } + + public async Task GetUser(string user) { + var response = await _client.GetAsync>( + "users/by/username/{user}", + new { user } + ); + return response!.Data; + } + + record TwitterSingleObject(T Data); + + public void Dispose() { + _client?.Dispose(); + GC.SuppressFinalize(this); + } +} +``` + +It is also possible to use ASP.NET Core Options for configuring the client, instead of passing the credentials as strings. For example, we can add a class for Twitter client options, and use it in a constructor: + +```csharp +public class TwitterClientOptions(string ApiKey, string ApiSecret); + +public TwitterClient(IOptions options) { + var opt = new RestClientOptions("https://api.twitter.com/2"); + _client = new RestClient(options); +} +``` + +Then, you can register and configure the client using ASP.NET Core dependency injection container. + +Right now, the client won't really work as Twitter API requires authentication. It's covered in the next section. + +## Authenticator + +Before we can call the API itself, we need to get a bearer token. Twitter exposes an endpoint `https://api.twitter.com/oauth2/token`. As it follows the OAuth2 conventions, the code can be used to create an authenticator for some other vendors. + +First, we need a model for deserializing the token endpoint response. OAuth2 uses snake case for property naming, so we need to decorate model properties with `JsonPropertyName` attribute: + +```csharp +record TokenResponse { + [JsonPropertyName("token_type")] + public string TokenType { get; init; } + [JsonPropertyName("access_token")] + public string AccessToken { get; init; } +} +``` + +Next, we create the authenticator itself. It needs the API key and API key secret to call the token endpoint using basic HTTP authentication. In addition, we can extend the list of parameters with the base URL to convert it to a more generic OAuth2 authenticator. + +The easiest way to create an authenticator is to inherit from the `AuthenticatorBase` base class: + +```csharp +public class TwitterAuthenticator : AuthenticatorBase { + readonly string _baseUrl; + readonly string _clientId; + readonly string _clientSecret; + + public TwitterAuthenticator(string baseUrl, string clientId, string clientSecret) : base("") { + _baseUrl = baseUrl; + _clientId = clientId; + _clientSecret = clientSecret; + } + + protected override async ValueTask GetAuthenticationParameter(string accessToken) { + Token = string.IsNullOrEmpty(Token) ? await GetToken() : Token; + return new HeaderParameter(KnownHeaders.Authorization, Token); + } +} +``` + +During the first call made by the client using the authenticator, it will find out that the `Token` property is empty. It will then call the `GetToken` function to get the token once and reuse the token going forward. + +Now, we need to implement the `GetToken` function in the class: + +```csharp +async Task GetToken() { + var options = new RestClientOptions(_baseUrl){ + Authenticator = new HttpBasicAuthenticator(_clientId, _clientSecret), + }; + using var client = new RestClient(options); + + var request = new RestRequest("oauth2/token") + .AddParameter("grant_type", "client_credentials"); + var response = await client.PostAsync(request); + return $"{response!.TokenType} {response!.AccessToken}"; +} +``` + +As we need to make a call to the token endpoint, we need our own short-lived instance of `RestClient`. Unlike the actual Twitter client, it will use the `HttpBasicAuthenticator` to send the API key and secret as the username and password. The client then gets disposed as we only use it once. + +Here we add a POST parameter `grant_type` with `client_credentials` as its value. At the moment, it's the only supported value. + +The POST request will use the `application/x-www-form-urlencoded` content type by default. + +::: note +Sample code provided on this page is a production code. For example, the authenticator might produce undesired side effect when multiple requests are made at the same time when the token hasn't been obtained yet. It can be solved rather than simply using semaphores or synchronized invocation. +::: + +## Final words + +This page demonstrates how an API client can be implemented as a typed, configurable client with its own interface. Usage of the client in applications is not covered here as different application types and target frameworks have their own idiomatic ways to use HTTP clients. \ No newline at end of file diff --git a/docs/docs/usage/execute.md b/docs/docs/usage/execute.md new file mode 100644 index 000000000..36343f793 --- /dev/null +++ b/docs/docs/usage/execute.md @@ -0,0 +1,172 @@ +--- +sidebar_position: 5 +title: Making calls +--- + +## Executing requests + +Once you've added all the parameters to your `RestRequest`, you are ready to make a request. + +`RestClient` has a single function for this: + +```csharp +public async Task ExecuteAsync( + RestRequest request, + CancellationToken cancellationToken = default +) +``` + +You can also avoid setting the request method upfront and use one of the overloads: + +```csharp +Task ExecuteGetAsync(RestRequest request, CancellationToken cancellationToken) +Task ExecutePostAsync(RestRequest request, CancellationToken cancellationToken) +Task ExecutePutAsync(RestRequest request, CancellationToken cancellationToken) +Task ExecuteDeleteAsync(RestRequest request, CancellationToken cancellationToken) +Task ExecuteHeadAsync(RestRequest request, CancellationToken cancellationToken) +Task ExecuteOptionsAsync(RestRequest request, CancellationToken cancellationToken) +``` + +When using any of those methods, you will get the response content as string in `response.Content`. + +RestSharp can deserialize the response for you. To use that feature, use one of the generic overloads: + +```csharp +Task> ExecuteAsync(RestRequest request, CancellationToken cancellationToken) +Task> ExecuteGetAsync(RestRequest request, CancellationToken cancellationToken) +Task> ExecutePostAsync(RestRequest request, CancellationToken cancellationToken) +Task> ExecutePutAsync(RestRequest request, CancellationToken cancellationToken) +Task> ExecuteDeleteAsync(RestRequest request, CancellationToken cancellationToken) +Task> ExecuteHeadAsync(RestRequest request, CancellationToken cancellationToken) +Task> ExecuteOptionsAsync(RestRequest request, CancellationToken cancellationToken) +``` + +:::note Beware of errors +All the overloads with names starting with `Execute` don't throw an exception if the server returns an error. Read more about it [here](../advanced/error-handling.md). +It allows you to inspect responses and handle remote server errors gracefully. Overloads without `Execute` prefix throw exceptions in case of any error, so you'd need to ensure to handle exceptions properly. +::: + +If you just need a deserialized response, you can use one of the extensions: + +```csharp +Task GetAsync(RestRequest request, CancellationToken cancellationToken) +Task PostAsync(RestRequest request, CancellationToken cancellationToken) +Task PutAsync(RestRequest request, CancellationToken cancellationToken) +Task PatchAsync(RestRequest request, CancellationToken cancellationToken) +Task DeleteAsync(RestRequest request, CancellationToken cancellationToken) +Task HeadAsync(RestRequest request, CancellationToken cancellationToken) +Task OptionsAsync(RestRequest request, CancellationToken cancellationToken) +``` + +Those extensions will throw an exception if the server returns an error, as there's no other way to float the error back to the caller. + +The `IRestClient` interface also has extensions for making requests without deserialization, which throw an exception if the server returns an error even if the client is configured to not throw exceptions. + +```csharp +Task GetAsync(RestRequest request, CancellationToken cancellationToken) +Task PostAsync(RestRequest request, CancellationToken cancellationToken) +Task PutAsync(RestRequest request, CancellationToken cancellationToken) +Task PatchAsync(RestRequest request, CancellationToken cancellationToken) +Task DeleteAsync(RestRequest request, CancellationToken cancellationToken) +Task HeadAsync(RestRequest request, CancellationToken cancellationToken) +Task OptionsAsync(RestRequest request, CancellationToken cancellationToken) +``` + +### Sync calls + +The preferred way for making requests is to execute them asynchronously as HTTP calls are IO-bound operations. +If you are unable to make async calls, all the functions about have sync overloads, which have the same names without `Async` suffix. +For example, for making a sync `GET` call you can use `ExecuteGet(request)` or `Get`, etc. + +## Requests without body + +Some HTTP methods don't suppose to be used with request body. For those methods, RestSharp supports making simplified calls without using `RestRequest`. All you need is to provide the resource path as a string. + +For example, you can make a `DELETE` call like this: + +```csharp +var response = await client.ExecuteDeleteAsync($"order/delete/{orderId}", cancellationToken); +``` + +Similarly, you can make `GET` calls with or without deserialization of the response using `ExecuteGetAsync(resource)`, `GetAsync(resource)`, `ExecuteGetAsync(resource)`, and `GetAsync(resource)` (see below). + +## JSON requests + +RestSharp provides an easier API for making calls to endpoints that accept and return JSON. + +### GET calls + +To make a simple `GET` call and get a deserialized JSON response with a pre-formed resource string, use this: + +```csharp +var response = await client.GetAsync("endpoint?foo=bar", cancellationToken); +``` + +:::note +In v111, `GetJsonAsync` is renamed to `GetAsync`. +::: + +You can also use a more advanced extension that uses an object to compose the resource string: + +```csharp +var client = new RestClient("https://example.org"); +var args = new { + id = "123", + foo = "bar" +}; +// Will make a call to https://example.org/endpoint/123?foo=bar +var response = await client.GetAsync("endpoint/{id}", args, cancellationToken); +``` + +It will search for the URL segment parameters matching any of the object properties and replace them with values. All the other properties will be used as query parameters. + +One note about `GetAsync` is that it will deserialize the response with any supported content type, not only JSON. + +### POST calls + +Similar things are available for `POST` requests. + +```csharp +var request = new CreateOrder("123", "foo", 10100); +// Will post the request object as JSON to "orders" and returns a +// JSON response deserialized to OrderCreated +var result = client.PostJsonAsync("orders", request, cancellationToken); +``` + +```csharp +var request = new CreateOrder("123", "foo", 10100); +// Will post the request object as JSON to "orders" and returns a +// status code, not expecting any response body +var statusCode = client.PostJsonAsync("orders", request, cancellationToken); +``` + +The same two extensions also exist for `PUT` requests (`PutJsonAsync`); + +## Downloading binary data + +There are two functions that allow you to download binary data from the remote API. + +First, there's `DownloadDataAsync`, which returns `Task`. This function allows you to open a stream reader and asynchronously stream large responses to memory or disk. + +## JSON streaming + +For HTTP API endpoints that stream the response data (like [Twitter search stream](https://developer.twitter.com/en/docs/twitter-api/tweets/filtered-stream/api-reference/get-tweets-search-stream)) you can use RestSharp with `StreamJsonAsync`, which returns an `IAsyncEnumerable`: + +```csharp +public async IAsyncEnumerable SearchStream( + [EnumeratorCancellation] CancellationToken cancellationToken = default +) { + var response = _client.StreamJsonAsync>( + "tweets/search/stream", cancellationToken + ); + + await foreach (var item in response.WithCancellation(cancellationToken)) { + yield return item.Data; + } +} +``` + +The main limitation of this function is that it expects each JSON object to be returned as a single line. It is unable to parse the response by combining multiple lines into a JSON string. + diff --git a/docs/docs/usage/request.md b/docs/docs/usage/request.md new file mode 100644 index 000000000..374ee14dd --- /dev/null +++ b/docs/docs/usage/request.md @@ -0,0 +1,350 @@ +--- +sidebar_position: 4 +title: Preparing requests +--- + +## Create a request + +Before making a request using `RestClient`, you need to create a request instance: + +```csharp +var request = new RestRequest(resource); // resource is the sub-path of the client base path +``` + +The default request type is `GET` and you can override it by setting the `Method` property. You can also set the method using the constructor overload: + +```csharp +var request = new RestRequest(resource, Method.Post); +``` + +After you've created a `RestRequest`, you can add parameters to it. Below, you can find all the parameter types supported by RestSharp. + +## Request headers + +Adds the header parameter as an HTTP header that is sent along with the request. The header name is the parameter's name and the header value is the value. + +You can use one of the following request methods to add a header parameter: + +```csharp +AddHeader(string name, string value); +AddHeader(string name, T value); // value will be converted to string +AddOrUpdateHeader(string name, string value); // replaces the header if it already exists +``` + +For example: + +```csharp +var request = new RestRequest("/path").AddHeader("X-Key", someKey); +``` + +You can also add header parameters to the client, and they will be added to every request made by the client. This is useful for adding authentication headers, for example. + +```csharp +client.AddDefaultHeader(string name, string value); +``` + +:::warning Avoid setting Content-Type header +RestSharp will use the correct content type by default. Avoid adding the `Content-Type` header manually to your requests unless you are absolutely sure it is required. You can add a custom content type to the [body parameter](#request-body) itself. +::: + +## Get or Post parameters + +The default RestSharp parameter type is `GetOrPostParameter`. You can add `GetOrPost` parameter to the request using the `AddParameter` function: + +```csharp +request + .AddParameter("name1", "value1") + .AddParameter("name2", "value2"); +``` + +`GetOrPost` behaves differently based on the HTTP method. If you execute a `GET` call, RestSharp will append the parameters to the URL in the form `url?name1=value1&name2=value2`. + +On a `POST` or `PUT` requests, it depends on whether you have files attached to a request. +If not, the parameters will be sent as the body of the request in the form `name1=value1&name2=value2`. Also, the request will be sent as `application/x-www-form-urlencoded`. + +In both cases, name and value will automatically be URL-encoded, unless specified otherwise: + +```csharp +request.AddParameter("name", "Væ üé", false); // don't encode the value +``` + +If you have files, RestSharp will send a `multipart/form-data` request. Your parameters will be part of this request in the form: + +``` +Content-Type: text/plain; charset=utf-8 +Content-Disposition: form-data; name="parameterName" + +ParameterValue +``` + +Sometimes, you need to override the default content type for the parameter when making a multipart form call. It's possible to do by setting the `ContentType` property of the parameter object. As an example, the code below will create a POST parameter with JSON value, and set the appropriate content type: + +```csharp +var parameter = new GetOrPostParameter("someJson", "{\"attributeFormat\":\"pdf\"}") { + ContentType = "application/json" +}; +request.AddParameter(parameter); +``` + +When the request is set to use multipart content, the parameter will be sent as part of the request with the specified content type: + +``` +Content-Type: application/json; charset=utf-8 +Content-Disposition: form-data; name="someJson" + +{"attributeFormat":"pdf"} +``` + +You can also add `GetOrPost` parameter as a default parameter to the client. This will add the parameter to every request made by the client. + +```csharp +client.AddDefaultParameter("foo", "bar"); +``` + +It will work the same way as request parameters, except that it will be added to every request. + +## Query string + +`QueryString` works like `GetOrPost`, except that it always appends the parameters to the url in the form `url?name1=value1&name2=value2`, regardless of the request method. + +Example: + +```csharp +var client = new RestClient("https://search.me"); +var request = new RestRequest("search") + .AddParameter("foo", "bar"); +var response = await client.GetAsync(request); +``` + +It will send a `GET` request to `https://search.me/search?foo=bar`. + +For `POST`-style requests you need to add the query string parameter explicitly: + +```csharp +request.AddQueryParameter("foo", "bar"); +``` + +In some cases, you might need to prevent RestSharp from encoding the query string parameter. +To do so, set the `encode` argument to `false` when adding the parameter: + +```csharp +request.AddQueryParameter("foo", "bar/fox", false); +``` + +You can also add a query string parameter as a default parameter to the client. This will add the parameter to every request made by the client. + +```csharp +client.AddDefaultQueryParameter("foo", "bar"); +``` + +The line above will result in all the requests made by that client instance to have `foo=bar` in the query string for all the requests made by that client. + +## Using AddObject + +You can avoid calling `AddParameter` multiple times if you collect all the parameters in an object, and then use `AddObject`. +For example, this code: + +```csharp +var params = new { + status = 1, + priority = "high", + ids = new [] { "123", "456" } +}; +request.AddObject(params); +``` + +is equivalent to: + +```csharp +request.AddParameter("status", 1); +request.AddParameter("priority", "high"); +request.AddParameter("ids", "123,456"); +``` + +Remember that `AddObject` only works if your properties have primitive types. It also works with collections of primitive types as shown above. + +If you need to override the property name or format, you can do it using the `RequestProperty` attribute. For example: + +```csharp +public class RequestModel { + // override the name and the format + [RequestProperty(Name = "from_date", Format = "d")] + public DateTime FromDate { get; set; } +} + +// add it to the request +request.AddObject(new RequestModel { FromDate = DateTime.Now }); +``` + +In this case, the request will get a GET or POST parameter named `from_date` and its value would be the current date in short date format. + +## Using AddObjectStatic + +Request function `AddObjectStatic(...)` allows using pre-compiled expressions for getting property values. Compared to `AddObject` that uses reflections for each call, `AddObjectStatic` caches functions to retrieve properties from an object of type `T`, so it works much faster. + +You can instruct `AddObjectStatic` to use custom parameter names and formats, as well as supply the list of properties than need to be used as parameters. The last option could be useful if the type `T` has properties that don't need to be sent with HTTP call. + +To use custom parameter name or format, use the `RequestProperty` attribute. For example: + +```csharp +class TestObject { + [RequestProperty(Name = "some_data")] + public string SomeData { get; set; } + + [RequestProperty(Format = "d")] + public DateTime SomeDate { get; set; } + + [RequestProperty(Name = "dates", Format = "d")] + public DateTime[] DatesArray { get; set; } + + public int Plain { get; set; } + public DateTime[] PlainArray { get; set; } +} +``` + +## URL segment parameter + +Unlike `GetOrPost`, URL segment parameter replaces placeholder values in the request URL: + +```csharp +var request = new RestRequest("health/{entity}/status") + .AddUrlSegment("entity", "s2"); +``` + +When the request executes, RestSharp will try to match any `{placeholder}` with a parameter of that name (without the `{}`) and replace it with the value. So the above code results in `health/s2/status` being the URL. + +You can also add `UrlSegment` parameter as a default parameter to the client. This will add the parameter to every request made by the client. + +```csharp +client.AddDefaultUrlSegment("foo", "bar"); +``` + +## Cookies + +You can add cookies to a request using the `AddCookie` method: + +```csharp +request.AddCookie("foo", "bar"); +``` + +RestSharp will add cookies from the request as cookie headers and then extract the matching cookies from the response. You can observe and extract response cookies using the `RestResponse.Cookies` properties, which has the `CookieCollection` type. + +However, the usage of a default URL segment parameter is questionable as you can just include the parameter value to the base URL of the client. There is, however, a `CookieContainer` instance on the request level. You can either assign the pre-populated container to `request.CookieContainer`, or let the container be created by the request when you call `AddCookie`. Still, the container is only used to extract all the cookies from it and create cookie headers for the request instead of using the container directly. It's because the cookie container is normally configured on the `HttpClientHandler` level and cookies are shared between requests made by the same client. In most of the cases this behaviour can be harmful. + +If your use case requires sharing cookies between requests made by the client instance, you can use the client-level `CookieContainer`, which you must provide as the options' property. You can add cookies to the container using the container API. No response cookies, however, would be auto-added to the container, but you can do it in code by getting cookies from the `Cookes` property of the response and adding them to the client-level container available via `IRestClient.Options.CookieContainer` property. + +## Request Body + +RestSharp supports multiple ways to add a request body: +- `AddJsonBody` for JSON payloads +- `AddXmlBody` for XML payloads +- `AddStringBody` for pre-serialized payloads + +We recommend using `AddJsonBody` or `AddXmlBody` methods instead of `AddParameter` with type `BodyParameter`. Those methods will set the proper request type and do the serialization work for you. + +When you make a `POST`, `PUT` or `PATCH` request and added `GetOrPost` [parameters](#get-or-post-parameters), RestSharp will send them as a URL-encoded form request body by default. When a request also has files, it will send a `multipart/form-data` request. You can also instruct RestSharp to send the body as `multipart/form-data` by setting the `AlwaysMultipartFormData` property to `true`. + +You can specify a custom body content type if necessary. The `contentType` argument is available in all the overloads that add a request body. + +It is not possible to add client-level default body parameters. + +### String body + +If you have a pre-serialized payload like a JSON string, you can use `AddStringBody` to add it as a body parameter. You need to specify the content type, so the remote endpoint knows what to do with the request body. For example: + +```csharp +const json = "{ data: { foo: \"bar\" } }"; +request.AddStringBody(json, ContentType.Json); +``` + +### JSON body + +When you call `AddJsonBody`, it does the following for you: + +- Instructs the RestClient to serialize the object parameter as JSON when making a request +- Sets the content type to `application/json` +- Sets the internal data type of the request body to `DataType.Json` + +Here is the example: + +```csharp +var param = new MyClass { IntData = 1, StringData = "test123" }; +request.AddJsonBody(param); +``` + +It is possible to override the default content type by supplying the `contentType` argument. For example: + +```csharp +request.AddJsonBody(param, "text/x-json"); +``` + +If you use a pre-serialized string with `AddJsonBody`, it will be sent as-is. The `AddJsonBody` will detect if the parameter is a string and will add it as a string body with JSON content type. +Essentially, it means that top-level strings won't be serialized as JSON when you use `AddJsonBody`. To overcome this issue, you can use an overload of `AddJsonBody`, which allows you to tell RestSharp to serialize the string as JSON: + +```csharp +const string payload = @" +""requestBody"": { + ""content"": { + ""application/json"": { + ""schema"": { + ""type"": ""string"" + } + } + } +},"; +request.AddJsonBody(payload, forceSerialize: true); // the string will be serialized +request.AddJsonBody(payload); // the string will NOT be serialized and will be sent as-is +``` + +### XML body + +When you call `AddXmlBody`, it does the following for you: + +- Instructs the RestClient to serialize the object parameter as XML when making a request +- Sets the content type to `application/xml` +- Sets the internal data type of the request body to `DataType.Xml` + +:::warning +Do not send XML string to `AddXmlBody`; it won't work! +::: + +## Uploading files + +To add a file to the request you can use the `RestRequest` function called `AddFile`. The main function accepts the `FileParameter` argument: + +```csharp +request.AddFile(fileParameter); +``` + +You can instantiate the file parameter using `FileParameter.Create` that accepts a bytes array, or `FileParameter.FromFile`, which will load the file from disk. + +There are also extension functions that wrap the creation of `FileParameter` inside: + +```csharp +// Adds a file from disk +AddFile(parameterName, filePath, contentType); + +// Adds an array of bytes +AddFile(parameterName, bytes, fileName, contentType); + +// Adds a stream returned by the getFile function +AddFile(parameterName, getFile, fileName, contentType); +``` + +Remember that `AddFile` will set all the necessary headers, so please don't try to set content headers manually. + +You can also provide file upload options to the `AddFile` call. The options are: +- `DisableFilenameEncoding` (default `false`): if set to `true`, RestSharp will not encode the file name in the `Content-Disposition` header +- `DisableFilenameStar` (default `true`): if set to `true`, RestSharp will not add the `filename*` parameter to the `Content-Disposition` header + +Example of using the options: + +```csharp +var options = new FileParameterOptions { + DisableFilenameEncoding = true, + DisableFilenameStar = false +}; +request.AddFile("file", filePath, options: options); +``` + +The options specified in the snippet above usually help when you upload files with non-ASCII characters in their names. diff --git a/docs/docs/usage/response.md b/docs/docs/usage/response.md new file mode 100644 index 000000000..dbaf302b4 --- /dev/null +++ b/docs/docs/usage/response.md @@ -0,0 +1,36 @@ +--- +sidebar_position: 6 +title: Handling responses +--- + +All `Execute{Method}Async` functions return an instance of `RestResponse`. Similarly, `Execute{Method}Async` return a generic instance of `RestResponse` where `T` is the response object type. + +Response object contains the following properties: + +| Property | Type | Description | +|--------------------------|-----------------------------------------------------|------------------------------------------------------------------------------------------| +| `Request` | `RestRequest` | Request instance that was used to get the response. | +| `ContentType` | `string?` | Response content type. `Null` if response has no content. | +| `ContentLength` | `long?` | Response content length. `Null` if response has no content. | +| `ContentEncoding` | `ICollection` | Content encoding collection. Empty if response has no content. | +| `Content` | `string?` | Response content as string. `Null` if response has no content. | +| `IsSuccessfulStatusCode` | `bool` | Indicates if response was successful, so no errors were reported by the server. | +| `ResponseStatus` | `None`, `Completed`, `Error`, `TimedOut`, `Aborted` | Response completion status. Note that completed responses might still return errors. | +| `IsSuccessful` | `bool` | `True` when `IsSuccessfulStatusCode` is `true` and `ResponseStatus` is `Completed`. | +| `StatusDescription` | `string?` | Response status description, if available. | +| `RawBytes` | `byte[]?` | Response content as byte array. `Null` if response has no content. | +| `ResponseUri` | `Uri?` | URI of the response, which might be different from request URI in case of redirects. | +| `Server` | `string?` | Server header value of the response. | +| `Cookies` | `CookieCollection?` | Collection of cookies received with the response, if any. | +| `Headers` | Collection of `HeaderParameter` | Response headers. | +| `ContentHeaders` | Collection of `HeaderParameter` | Response content headers. | +| `ErrorMessage` | `string?` | Transport or another non-HTTP error generated while attempting request. | +| `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. | + +In addition, `RestResponse` has one additional property: + +| Property | Type | Description | +|----------|------|-----------------------------------------------------------------------------------------------------------------------------------------------------------| +| `Data` | `T?` | Deserialized response object. `Null` if there's no content in the response, deserializer failed to understand the response content, or if request failed. | diff --git a/docs/docusaurus.config.ts b/docs/docusaurus.config.ts new file mode 100644 index 000000000..1000950fa --- /dev/null +++ b/docs/docusaurus.config.ts @@ -0,0 +1,138 @@ +import type {Config} from "@docusaurus/types"; +import type * as Preset from "@docusaurus/preset-classic"; +import {themes} from "prism-react-renderer"; + +const config: Config = { + title: "RestSharp", + tagline: "Simple REST and HTTP API Client for .NET", + favicon: "img/favicon.ico", + url: "https://restsharp.dev", + baseUrl: "/", + onBrokenLinks: "throw", + i18n: { + defaultLocale: "en", + locales: ["en"], + }, + markdown: { + hooks: { + onBrokenMarkdownLinks: "warn", + } + }, + plugins: [ + [ + '@docusaurus/plugin-client-redirects', + { + redirects: [ + { + from: '/v107', + to: '/migration', + }, + ], + }, + ], + ], + + presets: [[ + "classic", { + docs: { + editUrl: "https://github.com/RestSharp/RestSharp/tree/dev/docs", + sidebarPath: "./sidebars.ts", + includeCurrentVersion: true, + versions: { + "v111": { + label: "v111" + }, + "v110": { + label: "v110" + } + } + }, + theme: { + customCss: "./src/css/custom.css", + }, + } satisfies Preset.Options, + ]], + + themeConfig: { + navbar: { + title: "RestSharp", + logo: { + alt: "RestSharp Logo", + src: "img/restsharp.png", + }, + items: [ + { + type: "docSidebar", + sidebarId: "tutorialSidebar", + position: "left", + label: "Documentation", + }, + { + href: "/migration", + label: "Migration from v106" + }, + { + href: "/support", + label: "Support", + }, + { + type: "docsVersionDropdown", + position: "right", + }, + { + href: 'https://github.com/RestSharp/RestSharp', + label: "GitHub", + position: "right", + }, + ], + }, + footer: { + style: "dark", + links: [ + { + title: "Docs", + items: [ + { + label: "Documentation", + to: "/docs/intro", + }, + ], + }, + { + title: "Community", + items: [ + { + label: "Stack Overflow", + href: "https://stackoverflow.com/questions/tagged/restsharp", + }, + { + label: "Discord", + href: "https://discordapp.com/invite/docusaurus", + }, + { + label: "Twitter", + href: "https://twitter.com/RestSharp", + }, + ], + }, + { + title: "More", + items: [ + { + label: "GitHub", + href: "https://github.com/RestSharp/RestSharp", + }, + ], + }, + ], + copyright: `Copyright © ${new Date().getFullYear()} .NET Foundation. Built with Docusaurus.`, + }, + prism: { + theme: themes.vsLight, + darkTheme: themes.vsDark, + additionalLanguages: ['csharp'], + }, + } satisfies Preset.ThemeConfig, +}; + +export default config; diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 000000000..ff37c28c3 --- /dev/null +++ b/docs/package.json @@ -0,0 +1,46 @@ +{ + "name": "docs", + "version": "0.0.0", + "private": true, + "scripts": { + "docusaurus": "docusaurus", + "start": "docusaurus start", + "build": "docusaurus build", + "swizzle": "docusaurus swizzle", + "deploy": "docusaurus deploy", + "clear": "docusaurus clear", + "serve": "docusaurus serve", + "write-translations": "docusaurus write-translations", + "write-heading-ids": "docusaurus write-heading-ids", + "typecheck": "tsc" + }, + "dependencies": { + "@docusaurus/core": "^3.9.2", + "@docusaurus/plugin-client-redirects": "^3.9.2", + "@docusaurus/preset-classic": "^3.9.2", + "@mdx-js/react": "^3.1.1", + "clsx": "^2.1.1", + "prism-react-renderer": "^2.4.1", + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@docusaurus/module-type-aliases": "^3.9.2", + "@docusaurus/tsconfig": "^3.9.2", + "@docusaurus/types": "^3.9.2", + "typescript": "~5.9.3" + }, + "browserslist": { + "production": [ + ">0.5%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 3 chrome version", + "last 3 firefox version", + "last 5 safari version" + ] + }, + "packageManager": "pnpm@10.10.0" +} diff --git a/docs/sidebars.ts b/docs/sidebars.ts new file mode 100644 index 000000000..431ed961b --- /dev/null +++ b/docs/sidebars.ts @@ -0,0 +1,21 @@ +import type {SidebarsConfig} from '@docusaurus/plugin-content-docs'; + +const sidebars: SidebarsConfig = { + // By default, Docusaurus generates a sidebar from the docs folder structure + tutorialSidebar: [{type: 'autogenerated', dirName: '.'}], + + // But you can create a sidebar manually + /* + tutorialSidebar: [ + 'intro', + 'hello', + { + type: 'category', + label: 'Tutorial', + items: ['tutorial-basics/create-a-document'], + }, + ], + */ +}; + +export default sidebars; diff --git a/docs/src/components/HomepageFeatures/index.tsx b/docs/src/components/HomepageFeatures/index.tsx new file mode 100644 index 000000000..fd953b7c6 --- /dev/null +++ b/docs/src/components/HomepageFeatures/index.tsx @@ -0,0 +1,70 @@ +import clsx from "clsx"; +import Heading from "@theme/Heading"; +import styles from "./styles.module.css"; + +type FeatureItem = { + title: string; + // Svg: React.ComponentType>; + description: JSX.Element; +}; + +const FeatureList: FeatureItem[] = [ + { + title: "Serialization", + // Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default, + description: ( + <> + Make calls using XML or JSON body, and receive XML or JSON responses. + RestSharp takes care of serializing requests and deserializing responses, as well as adding the correct content type. + + ), + }, + { + title: "Fully async", + // Svg: require('@site/static/img/undraw_docusaurus_tree.svg').default, + description: ( + <> + RestSharp API has an extensive number of async functions to make all sort of HTTP calls. + It still provides sync overloads to allow using RestSharp in legacy applications or non-async environments. + + ), + }, + { + title: "Parameters", + // Svg: require('@site/static/img/undraw_docusaurus_react.svg').default, + description: ( + <> + Whether you want to add a query, a URL, or URL-encoded form parameters, RestSharp allows doing it with one line of code. + The same applies to sending files and using multipart forms. + + ), + }, +]; + +function Feature({ title, description }: FeatureItem) { + return ( +
+ {/*
*/} + {/* */} + {/*
*/} +
+ {title} +

{description}

+
+
+ ); +} + +export default function HomepageFeatures(): JSX.Element { + return ( +
+
+
+ {FeatureList.map((props, idx) => ( + + ))} +
+
+
+ ); +} diff --git a/docs/src/components/HomepageFeatures/styles.module.css b/docs/src/components/HomepageFeatures/styles.module.css new file mode 100644 index 000000000..b248eb2e5 --- /dev/null +++ b/docs/src/components/HomepageFeatures/styles.module.css @@ -0,0 +1,11 @@ +.features { + display: flex; + align-items: center; + padding: 2rem 0; + width: 100%; +} + +.featureSvg { + height: 200px; + width: 200px; +} diff --git a/docs/src/css/custom.css b/docs/src/css/custom.css new file mode 100644 index 000000000..2bc6a4cfd --- /dev/null +++ b/docs/src/css/custom.css @@ -0,0 +1,30 @@ +/** + * Any CSS included here will be global. The classic template + * bundles Infima by default. Infima is a CSS framework designed to + * work well for content-centric websites. + */ + +/* You can override the default Infima variables here. */ +:root { + --ifm-color-primary: #2e8555; + --ifm-color-primary-dark: #29784c; + --ifm-color-primary-darker: #277148; + --ifm-color-primary-darkest: #205d3b; + --ifm-color-primary-light: #33925d; + --ifm-color-primary-lighter: #359962; + --ifm-color-primary-lightest: #3cad6e; + --ifm-code-font-size: 95%; + --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); +} + +/* For readability concerns, you should choose a lighter palette in dark mode. */ +[data-theme='dark'] { + --ifm-color-primary: #25c2a0; + --ifm-color-primary-dark: #21af90; + --ifm-color-primary-darker: #1fa588; + --ifm-color-primary-darkest: #1a8870; + --ifm-color-primary-light: #29d5b0; + --ifm-color-primary-lighter: #32d8b4; + --ifm-color-primary-lightest: #4fddbf; + --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); +} diff --git a/docs/src/pages/index.module.css b/docs/src/pages/index.module.css new file mode 100644 index 000000000..9f71a5da7 --- /dev/null +++ b/docs/src/pages/index.module.css @@ -0,0 +1,23 @@ +/** + * CSS files with the .module.css suffix will be treated as CSS modules + * and scoped locally. + */ + +.heroBanner { + padding: 4rem 0; + text-align: center; + position: relative; + overflow: hidden; +} + +@media screen and (max-width: 996px) { + .heroBanner { + padding: 2rem; + } +} + +.buttons { + display: flex; + align-items: center; + justify-content: center; +} diff --git a/docs/src/pages/index.tsx b/docs/src/pages/index.tsx new file mode 100644 index 000000000..0374fc9ac --- /dev/null +++ b/docs/src/pages/index.tsx @@ -0,0 +1,43 @@ +import clsx from 'clsx'; +import Link from '@docusaurus/Link'; +import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import Layout from '@theme/Layout'; +import HomepageFeatures from '@site/src/components/HomepageFeatures'; +import Heading from '@theme/Heading'; + +import styles from './index.module.css'; + +function HomepageHeader() { + const {siteConfig} = useDocusaurusContext(); + return ( +
+
+ RestSharp logo + + {siteConfig.title} + +

{siteConfig.tagline}

+
+ + Get Started + +
+
+
+ ); +} + +export default function Home(): JSX.Element { + const {siteConfig} = useDocusaurusContext(); + return ( + + +
+ +
+
+ ); +} diff --git a/docs/src/pages/markdown-page.md b/docs/src/pages/markdown-page.md new file mode 100644 index 000000000..9756c5b66 --- /dev/null +++ b/docs/src/pages/markdown-page.md @@ -0,0 +1,7 @@ +--- +title: Markdown page example +--- + +# Markdown page example + +You don't need React to write simple standalone pages. diff --git a/docs/src/pages/migration.md b/docs/src/pages/migration.md new file mode 100644 index 000000000..902472ed7 --- /dev/null +++ b/docs/src/pages/migration.md @@ -0,0 +1,339 @@ +--- +title: Migration from v106 and earlier +--- + +## New RestSharp + +RestSharp got a major upgrade in v107, which contains quite a few breaking changes. + +The most important change is that RestSharp stop using the legacy `HttpWebRequest` class, and uses well-known `HttpClient` instead. +This move solves lots of issues, like hanging connections due to improper `HttpClient` instance cache, updated protocols support, and many other problems. + +Another big change is that `SimpleJson` is retired completely from the code base. Instead, RestSharp uses `JsonSerializer` from the `System.Text.Json` package, which is the default serializer for ASP.NET Core. + +Finally, most of the interfaces are now gone. + +## Brief migration guide + +### RestClient and options + +The `IRestClient` interface is deprecated in v107, but brought back in v109. The new interface, however, has a much smaller API compared to previous versions. You will be using the `RestClient` class instance. + +Most of the client options are moved to `RestClientOptions`. If you can't find the option you used to set on `IRestClient`, check the options; it's probably there. + +This is how you can instantiate the client using the simplest possible way: + +```csharp +var client = new RestClient("https://api.myorg.com"); +``` + +For customizing the client, use `RestClientOptions`: + +```csharp +var options = new RestClientOptions("https://api.myorg.com") { + ThrowOnAnyError = true, + Timeout = TimeSpan.FromSeconds(1) +}; +var client = new RestClient(options); +``` + +You can still change serializers and add default parameters to the client. + +::: +Note that client options cannot be changed after the client is instantiated. +It's because the client constructor either uses those options to configure its internal `HttpClient`, `HttpMessageHandler` or for making requests. +Even though options that are used for making requests can be changed in theory, making those options mutable would make `RestClient` not thread-safe. +::: + +### RestClient lifecycle + +Do not instantiate `RestClient` for each HTTP call. RestSharp creates a new instance of `HttpClient` internally, and you will get lots of hanging connections, and eventually exhaust the connection pool. + +If you use a dependency-injection container, register your API client as a singleton. + +### Body parameters + +Beware that most of the code generators, like Postman C# code gen, generate code for RestSharp before v107, and that code is broken. Such code worked mostly due to the obscurity of previous RestSharp versions API. For example, Postman-generated code tells you to add the content-type header, and the accept header, which in many cases is an anti-pattern. It also posts JSON payload as string, where RestSharp provides you with serialization and deserialization of JSON out of the box. + +Therefore, please read the [current version](/docs/intro/) documentation and follow our guidelines when using RestSharp v107+. + +Some of the points to be aware of: +- `AddParameter("application/json", ..., ParameterType.RequestBody)` won't work, use `AddBody` instead, or better, `AddJsonBody`. +- `AddJsonBody("{ foo: 'bar' }")` won't work (and it never worked), use `AddStringBody`. `AddJsonBody` is for serializable objects, not for strings. +- If your `AddParameter(something, something, ParameterType.RequestBody)` doesn't work, try `AddBody` as it will do its best to figure out what kind of body you're adding. + +### Headers + +Lots of code out there that uses RestSharp has lines like: + +```csharp +request.AddHeader("Content-Type", "application/json"); +request.AddHeader("Accept", "application/json"); +``` + +This is **completely unnecessary**, and often harmful. The `Content-Type` header is the content header, not the request header. It might be different per individual part of the body when using multipart-form data, for example. RestSharp sets the correct content-type header automatically, based on your body format, so don't override it. +The `Accept` header is set by RestSharp automatically based on registered serializers. By default, both XML and JSON are supported. Only change the `Accept` header if you need something else, like binary streams, or plain text. + +### Making requests + +The `IRestRequest` interface is deprecated. You will be using the `RestRequest` class instance. + +You can still create a request as before: + +```csharp +var request = new RestRequest(); +``` + +Adding parameters hasn't changed much, except you cannot add cookie parameters to the request. It's because cookies are added to the `HttpMessageHandler` cookie container, which is not accessible inside the request class. + +```csharp +var request = new RestRequest() + .AddQueryParameter("foo", "bar") + .AddJsonBody(someObject); +``` + +Quite a few options previously available via `IRestRequest` are now in `RestClientOptions`. It's also because changing those options forced us to use a different HTTP message handler, and it caused hanging connections, etc. + +When you got a request instance, you can make a call: + +```csharp +var request = new RestRequest() + .AddQueryParameter("foo", "bar") + .AddJsonBody(someObject); +var response = await client.PostAsync(request, cancellationToken); +``` + +The `IRestResponse` interface is deprecated. You get an instance of `RestResponse` or `RestResponse` in return. + +You can also use a simplified API for making POST and PUT requests: + +```csharp +var request = new MyRequest { Data = "foo" }; +var response = await client.PostAsync(request, cancellationToken); +// response will be of type TResponse +``` + +This way you avoid instantiating `RestRequest` explicitly. + +### Using your own HttpClient + +`RestClient` class has two constructors, which accept either `HttpClient` or `HttpMessageHandler` instance. + +This way you can use a pre-configured `HttpClient` or `HttpMessageHandler`, customized for your needs. + +### Default serializers + +For JSON, RestSharp will use `JsonSerializer` from the `System.Text.Json` package. This package is now referenced by default, and it is the only dependency of the RestSharp NuGet package. + +The `Utf8` serializer package is deprecated as the package is not being updated. + +For XML requests and responses RestSharp uses `DotNetXmlSerializer` and `DotNetXmlDeserializer`. +Previously used default `XmlSerializer`, `XmlDeserializer`, and `XmlAttrobuteDeserializer` are moved to a separate package `RestSharp.Serializers.Xml`. + +### NTLM authentication + +The `NtlmAuthenticator` is deprecated. + +NTLM authenticator was doing nothing more than telling `WebRequest` to use certain credentials. Now with RestSharp, the preferred way would be to set the `Credentials` or `UseDefaultCredentials` property in `RestClientOptions`. + +The reason to remove it was that all other authenticators use `AuthenticatorBase`, which must return a parameter. In general, any authenticator is given a request before its made, so it can do something with it. NTLM doesn't work this way, it needs some settings to be provided for `HttpClientHandler`, which is set up before the `HttpClient` instance is created, and it happens once per RestClient instance, and it cannot be changed per request. + +### Delegating handlers + +You can easily build your own request/response pipeline, as you would with `HttpClient`. RestClient will create an `HttpMessageHandler` instance for its own use, using the options provided. You can, of course, provide your own instance of `HttpMessageHandler` as `RestSharpClient` has a constructor that accepts a custom handler and uses it to create an `HttpClient` instance. However, you'll be on your own with the handler configuration in this case. + +If you want to build a _pipeline_, use [delegating handlers](https://docs.microsoft.com/en-us/aspnet/web-api/overview/advanced/httpclient-message-handlers). For example, you can use `HttpTracer` to [debug your HTTP calls](https://github.com/BSiLabs/HttpTracer) like this: + +```csharp +var options = new RestClientOptions(_server.Url) { + ConfigureMessageHandler = handler => + new HttpTracerHandler(handler, new ConsoleLogger(), HttpMessageParts.All) +}; +var client = new RestClient(options); +``` + +## Recommended usage + +`RestClient` should be thread-safe. It holds an instance of `HttpClient` and `HttpMessageHandler` inside. +Do not instantiate the client for a single call, otherwise you get issues with hanging connections and connection pooling won't be possible. + +Do create typed API clients for your use cases. Use a single instance of `RestClient` internally in such an API client for making calls. +It would be similar to using typed clients using `HttpClient`, for example: + +```csharp +public class GitHubClient { + readonly RestClient _client; + + public GitHubClient() { + _client = new RestClient("https://api.github.com/") + .AddDefaultHeader(KnownHeaders.Accept, "application/vnd.github.v3+json"); + } + + public Task GetRepos() + => _client.GetJsonAsync("users/aspnet/repos"); +} +``` + +Do not use one instance of `RestClient` across different API clients. + +This documentation contains the complete example of a [Twitter API client](/docs/intro), which you can use as a reference. + +## Presumably solved issues + +The next RestSharp version presumably solves the following issues: +- Connection pool starvation +- Hanging open TCP connections +- Improper handling of async calls +- Various `SimpleJson` serialization quirks +- HTTP/2 support +- Intermediate certificate issue +- Uploading large files (use file parameters with `Stream`) +- Downloading large files (use `DownloadFileStreamAsync`) + +## Deprecated interfaces + +The following interfaces are removed from RestSharp: +- `IRestRequest` +- `IRestResponse` +- `IHttp` + +### Mocking + +Mocking an infrastructure component like RestSharp (or HttpClient) is not the best idea. Even if you check that all the parameters are added correctly to the request, your "unit test" will only give you a false sense of safety that your code actually works. But, you have no guarantee that the remote server will accept your request, or if you can handle the actual response correctly. + +However, since v109 you can still mock the `IRestClient` interface, but you only need to implement the `ExecuteAsync` method. The `ExecuteAsync` method is the only one that actually makes a call to the remote server. All other methods are just wrappers around it. + +The best way to test HTTP calls is to make some, using the actual service you call. However, you might still want to check if your API client forms requests in a certain way. You might also be sure about what the remote server responds to your calls with, so you can build a set of JSON (or XML) responses, so you can simulate remote calls. + +It is perfectly doable without using interfaces. As RestSharp uses `HttpClient` internally, it certainly uses `HttpMessageHandler`. Features like delegating handlers allow you to intercept the request pipeline, inspect the request, and substitute the response. You can do it yourself, or use a library like [MockHttp](https://github.com/richardszalay/mockhttp). They have an example provided in the repository README, so we have changed it for RestClient here: + +```csharp +var mockHttp = new MockHttpMessageHandler(); + +// Setup a respond for the user api (including a wildcard in the URL) +mockHttp.When("http://localhost/api/user/*") + .Respond("application/json", "{'name' : 'Test McGee'}"); // Respond with JSON + +// Instantiate the client normally, but replace the message handler +var client = new RestClient(new RestClientOptions { ConfigureMessageHandler = _ => mockHttp }); + +var request = new RestRequest("http://localhost/api/user/1234"); +var response = await client.GetAsync(request); + +// No network connection required +Console.Write(response.Content); // {'name' : 'Test McGee'} +``` + +### Reference + +Below, you can find members of `IRestClient` and `IRestRequest` with their corresponding status and location in the new API. + +| `IRestClient` member | Where is it now? | +|:------------------------------------------------------------------------------------------------|:----------------------------------------| +| `CookieContainer` | `RestClientOptions`, `RestRequest` | +| `AutomaticDecompression` | `RestClientOptions`, changed type | +| `MaxRedirects` | `RestClientOptions` | +| `UserAgent` | `RestClientOptions` | +| `Timeout` | `RestClientOptions`, `RestRequest` | +| `Authenticator` | `RestClientOptions`, `RestRequest` | +| `BaseUrl` | `RestClientOptions` | +| `Encoding` | `RestClientOptions` | +| `ThrowOnDeserializationError` | `RestClientOptions` | +| `FailOnDeserializationError` | `RestClientOptions` | +| `ThrowOnAnyError` | `RestClientOptions` | +| `PreAuthenticate` | `RestClientOptions` | +| `BaseHost` | `RestClientOptions` | +| `AllowMultipleDefaultParametersWithSameName` | `RestClientOptions` | +| `ClientCertificates` | `RestClientOptions` | +| `Proxy` | `RestClientOptions` | +| `CachePolicy` | `RestClientOptions`, changed type | +| `FollowRedirects` | `RestClientOptions` | +| `RemoteCertificateValidationCallback` | `RestClientOptions` | +| `Pipelined` | Not supported | +| `UnsafeAuthenticatedConnectionSharing` | Not supported | +| `ConnectionGroupName` | Not supported | +| `ReadWriteTimeout` | Not supported | +| `UseSynchronizationContext` | Not supported | +| `DefaultParameters` | `RestClient` | +| `Deserialize(IRestResponse response)` | `RestSerializers` | +| `BuildUri(IRestRequest request)` | Extension | +| `ExecuteAsync(IRestRequest request, CancellationToken cancellationToken)` | `RestClient` | +| `ExecuteAsync(IRestRequest request, Method httpMethod, CancellationToken cancellationToken)` | Extension | +| `ExecuteAsync(IRestRequest request, Method httpMethod, CancellationToken cancellationToken)` | Extension | +| `ExecuteAsync(IRestRequest request, CancellationToken cancellationToken)` | Extension | +| `ExecuteGetAsync(IRestRequest request, CancellationToken cancellationToken)` | Extension | +| `ExecutePostAsync(IRestRequest request, CancellationToken cancellationToken)` | Extension | +| `ExecuteGetAsync(IRestRequest request, CancellationToken cancellationToken)` | Extension | +| `ExecutePostAsync(IRestRequest request, CancellationToken cancellationToken)` | Extension | +| `Execute(IRestRequest request)` | Extension (since v108) | +| `Execute(IRestRequest request, Method httpMethod)` | Extension (since v108) | +| `Execute(IRestRequest request)` | Extension (since v108) | +| `Execute(IRestRequest request, Method httpMethod)` | Extension (since v108) | +| `DownloadData(IRestRequest request)` | Extension (since v108) | +| `ExecuteAsGet(IRestRequest request, string httpMethod)` | Extension `ExecuteGetA` (since v108) | +| `ExecuteAsPost(IRestRequest request, string httpMethod)` | Extension `ExecutePost` (since v108) | +| `ExecuteAsGet(IRestRequest request, string httpMethod)` | Extension `ExecuteGet` (since v108) | +| `ExecuteAsPost(IRestRequest request, string httpMethod)` | Extension `ExecutePost` (since v108) | +| `BuildUriWithoutQueryParameters(IRestRequest request)` | Extension | +| `ConfigureWebRequest(Action configurator)` | Removed | +| `AddHandler(string contentType, Func deserializerFactory)` | Removed | +| `RemoveHandler(string contentType)` | Removed | +| `ClearHandlers()` | Removed | + +| `IRestRequest` member | Where is it now? | +|:-------------------------------------------------------------------------------------------------------|:---------------------------------| +| `AlwaysMultipartFormData` | `RestRequest` | +| `JsonSerializer` | Deprecated | +| `XmlSerializer` | Deprecated | +| `AdvancedResponseWriter` | `RestRequest`, changed signature | +| `ResponseWriter` | `RestRequest`, changed signature | +| `Parameters` | `RestRequest` | +| `Files` | `RestRequest` | +| `Method` | `RestRequest` | +| `Resource` | `RestRequest` | +| `RequestFormat` | `RestRequest` | +| `RootElement` | `RestRequest` | +| `DateFormat` | `XmlRequest` | +| `XmlNamespace` | `XmlRequest` | +| `Credentials` | Removed, use `RestClientOptions` | +| `Timeout` | `RestRequest` | +| `ReadWriteTimeout` | Not supported | +| `Attempts` | `RestRequest` | +| `UseDefaultCredentials` | Removed, use `RestClientOptions` | +| `AllowedDecompressionMethods` | Removed, use `RestClientOptions` | +| `OnBeforeDeserialization` | `RestRequest` | +| `OnBeforeRequest` | `RestRequest`, changed signature | +| `Body` | Removed, use `Parameters` | +| `AddParameter(Parameter p)` | `RestRequest` | +| `AddFile(string name, string path, string contentType)` | Extension | +| `AddFile(string name, byte[] bytes, string fileName, string contentType)` | Extension | +| `AddFile(string name, Action writer, string fileName, long contentLength, string contentType)` | Extension | +| `AddFileBytes(string name, byte[] bytes, string filename, string contentType)` | Extension `AddFile` | +| `AddBody(object obj, string xmlNamespace)` | Deprecated | +| `AddBody(object obj)` | Extension | +| `AddJsonBody(object obj)` | Extension | +| `AddJsonBody(object obj, string contentType)` | Extension | +| `AddXmlBody(object obj)` | Extension | +| `AddXmlBody(object obj, string xmlNamespace)` | Extension | +| `AddObject(object obj, params string[] includedProperties)` | Extension | +| `AddObject(object obj)` | Extension | +| `AddParameter(string name, object value)` | Extension | +| `AddParameter(string name, object value, ParameterType type)` | Extension | +| `AddParameter(string name, object value, string contentType, ParameterType type)` | Extension | +| `AddOrUpdateParameter(Parameter parameter)` | Extension | +| `AddOrUpdateParameters(IEnumerable parameters)` | Extension | +| `AddOrUpdateParameter(string name, object value)` | Extension | +| `AddOrUpdateParameter(string name, object value, ParameterType type)` | Extension | +| `AddOrUpdateParameter(string name, object value, string contentType, ParameterType type)` | Extension | +| `AddHeader(string name, string value)` | Extension | +| `AddOrUpdateHeader(string name, string value)` | Extension | +| `AddHeaders(ICollection> headers)` | Extension | +| `AddOrUpdateHeaders(ICollection> headers)` | Extension | +| `AddCookie(string name, string value)` | Extension | +| `AddUrlSegment(string name, string value)` | Extension | +| `AddUrlSegment(string name, string value, bool encode)` | Extension | +| `AddUrlSegment(string name, object value)` | Extension | +| `AddQueryParameter(string name, string value)` | Extension | +| `AddQueryParameter(string name, string value, bool encode)` | Extension | +| `AddDecompressionMethod(DecompressionMethods decompressionMethod)` | Not supported | +| `IncreaseNumAttempts()` | Made internal | diff --git a/docs/src/pages/support.md b/docs/src/pages/support.md new file mode 100644 index 000000000..922293894 --- /dev/null +++ b/docs/src/pages/support.md @@ -0,0 +1,129 @@ +--- +id: "support" +--- + +# Get help + +Got issues, questions, suggestions? Please read this page carefully to understand how you can get help working with RestSharp. + +:::tip +You can also support maintainers and motivate them by contributing via [GitHub Sponsors](https://github.com/sponsors/restsharp). +::: + +## Questions + +The most effective way to resolve questions about using RestSharp is StackOverflow. + +RestSharp has a large user base. Tens of thousands of projects and hundreds of thousands of developers +use RestSharp on a daily basis. So, asking questions on **StackOverflow** with [restsharp](https://stackoverflow.com/questions/tagged/restsharp) tag +would most definitely lead you to a solution. + +:::warning +Please do not use GitHub issues to ask question about using RestSharp. +::: + +## Discussions + +We have a [mail list](http://groups.google.com/group/restsharp) at Google Groups dedicated to discussions about +using RestSharp, feature proposals and similar topics. It is perfectly fine to +ask questions about using RestSharp at that group too. + +Please check the group and engage with the community if you feel a need +to discuss things that you struggle with or want to improve. + +## Bugs and issues + +The last resort to get help when you experience some unexpected behaviour, +a crash or anything else that you consider a bug, is submitting an issue +at our GitHub repository. + +:::warning +**Do not ignore our contribution guidelines**, otherwise you risk your issue to be +closed without being considered. Respect the maintainers, be specific and provide +as many details about the issue as you can. +::: + +Ensure you provide the following in the issue: + - Expected behaviour + - Actual behaviour + - Why do you think it is an issue, not a misunderstanding + - How the issue can be reproduced: a repository or at least a code snippet + - If RestSharp unexpectedly throws an exception, provide the stack trace + +## Contributing + +Although issues are considered as contributions, we strongly suggest helping +the community by solving issues that you experienced by submitting a pull request. + +Here are contribution guidelines: + + - Make each pull request atomic and exclusive; don't send pull requests for a laundry list of changes. + - Even better, commit in small manageable chunks. + - Use the supplied `.editorconfig` file to format the code. + - Any change must be accompanied by a unit test covering the change. + - New tests are preferred to use FluentAssertions. + - No regions. + - No licence header for tested. + - Code must build for .NET Standard 2.0, .NET 5, and .NET 6. + - Test must run on .NET 6. + - Use `autocrlf=true` (`git config --global core.autocrlf true`) + + +## Common issues + +Before opening an issue on GitHub, please check the list of known issues below. + +### Content type + +One of the mistakes developers make when using RestSharp is setting the `Content-Type` header manually. +Remember that in most of the usual scenarios setting the content type header manually is not required, and it might be harmful. + +RestSharp sets the content type header automatically based on the request type. +You might want to override the request body content type, but the best way to do it is to supply the content type to the body parameter itself. +Functions for adding the request body to the request have overloads, which accept content type. For example + +```csharp +request.AddStringBody(jsonString, ContentType.Json); +``` + +### Setting the User Agent + +Setting the user agent on the request won't work when you use `AddHeader`. + +Instead, please use the `RestClientOptions.UserAgent` property. + +### Empty response + +We regularly get issues where developers complain that their requests get executed +and they get a proper raw response, but the `RestResponse` instance doesn't +have a deserialized object set. + +In other situations, the raw response is also empty. + +All those issues are caused by the design choice to swallow exceptions +that occur when RestSharp makes the request and processes the response. Instead, +RestSharp produces so-called _error response_. + +You can check the response status to find out if there are any errors. +The following properties can tell you about those errors: + +- `IsSuccessful` +- `ResponseStatus` +- `StatusCode` +- `ErrorMessage` +- `ErrorException` + +It could be that the request was executed and you got `200 OK` status +code back and some content, but the typed `Data` property is empty. + +In that case, you probably got deserialization issues. By default, RestSharp will just return an empty (`null`) result in the `Data` property. +Deserialization errors can be also populated to the error response. To do that, +set the `client.FailOnDeserializationError` property to `true`. + +It is also possible to force RestSharp to throw an exception. + +If you set `client.ThrowOnDeserializationError`, RestSharp will throw a `DeserializationException` +when the serializer throws. The exception has the internal exception and the response. + +Finally, by setting `ThrowOnAnyError` you can force RestSharp to re-throw any +exception that happens when making the request and processing the response. diff --git a/docs/static/.nojekyll b/docs/static/.nojekyll new file mode 100644 index 000000000..e69de29bb diff --git a/docs/static/img/android-chrome-192x192.png b/docs/static/img/android-chrome-192x192.png new file mode 100644 index 000000000..9a2cc6bb1 Binary files /dev/null and b/docs/static/img/android-chrome-192x192.png differ diff --git a/docs/static/img/android-chrome-512x512.png b/docs/static/img/android-chrome-512x512.png new file mode 100644 index 000000000..eaf07aa31 Binary files /dev/null and b/docs/static/img/android-chrome-512x512.png differ diff --git a/docs/static/img/apple-touch-icon.png b/docs/static/img/apple-touch-icon.png new file mode 100644 index 000000000..55567fb41 Binary files /dev/null and b/docs/static/img/apple-touch-icon.png differ diff --git a/docs/static/img/aws_logo.png b/docs/static/img/aws_logo.png new file mode 100644 index 000000000..68304e083 Binary files /dev/null and b/docs/static/img/aws_logo.png differ diff --git a/docs/static/img/favicon-16x16.png b/docs/static/img/favicon-16x16.png new file mode 100644 index 000000000..65435396c Binary files /dev/null and b/docs/static/img/favicon-16x16.png differ diff --git a/docs/static/img/favicon-32x32.png b/docs/static/img/favicon-32x32.png new file mode 100644 index 000000000..e938113bf Binary files /dev/null and b/docs/static/img/favicon-32x32.png differ diff --git a/docs/static/img/favicon.ico b/docs/static/img/favicon.ico new file mode 100644 index 000000000..d501014d2 Binary files /dev/null and b/docs/static/img/favicon.ico differ diff --git a/docs/static/img/restsharp.png b/docs/static/img/restsharp.png new file mode 100644 index 000000000..7203b38fd Binary files /dev/null and b/docs/static/img/restsharp.png differ diff --git a/docs/static/img/site.webmanifest b/docs/static/img/site.webmanifest new file mode 100644 index 000000000..45dc8a206 --- /dev/null +++ b/docs/static/img/site.webmanifest @@ -0,0 +1 @@ +{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} \ No newline at end of file diff --git a/docs/tsconfig.json b/docs/tsconfig.json new file mode 100644 index 000000000..314eab8a4 --- /dev/null +++ b/docs/tsconfig.json @@ -0,0 +1,7 @@ +{ + // This file is not used in compilation. It is here just for a nice editor experience. + "extends": "@docusaurus/tsconfig", + "compilerOptions": { + "baseUrl": "." + } +} diff --git a/docs/versioned_docs/version-v110/advanced/_category_.json b/docs/versioned_docs/version-v110/advanced/_category_.json new file mode 100644 index 000000000..f395bdfe4 --- /dev/null +++ b/docs/versioned_docs/version-v110/advanced/_category_.json @@ -0,0 +1,7 @@ +{ + "label": "Advanced topics", + "position": 4, + "link": { + "type": "generated-index" + } +} diff --git a/docs/versioned_docs/version-v110/advanced/authenticators.md b/docs/versioned_docs/version-v110/advanced/authenticators.md new file mode 100644 index 000000000..14196d97c --- /dev/null +++ b/docs/versioned_docs/version-v110/advanced/authenticators.md @@ -0,0 +1,185 @@ +# Authenticators + +RestSharp includes authenticators for basic HTTP, OAuth1 and token-based (JWT and OAuth2). + +There are two ways to set the authenticator: client-wide or per-request. + +Set the client-wide authenticator by assigning the `Authenticator` property of `RestClientOptions`: + +```csharp +var options = new RestClientOptions("https://example.com") { + Authenticator = new HttpBasicAuthenticator("username", "password") +}; +var client = new RestClient(options); +``` + +To set the authenticator per-request, assign the `Authenticator` property of `RestRequest`: + +```csharp +var request = new RestRequest("/api/users/me") { + Authenticator = new HttpBasicAuthenticator("username", "password") +}; +var response = await client.ExecuteAsync(request, cancellationToken); +``` + +## Basic authentication + +The `HttpBasicAuthenticator` allows you pass a username and password as a basic `Authorization` header using a base64 encoded string. + +```csharp +var options = new RestClientOptions("https://example.com") { + Authenticator = new HttpBasicAuthenticator("username", "password") +}; +var client = new RestClient(options); +``` + +## OAuth1 + +For OAuth1 authentication the `OAuth1Authenticator` class provides static methods to help generate an OAuth authenticator. +OAuth1 authenticator will add the necessary OAuth parameters to the request, including signature. + +The authenticator will use `HMAC SHA1` to create a signature by default. +Each static function to create the authenticator allows you to override the default and use another method to generate the signature. + +### Request token + +Getting a temporary request token is the usual first step in the 3-legged OAuth1 flow. +Use `OAuth1Authenticator.ForRequestToken` function to get the request token authenticator. +This method requires a `consumerKey` and `consumerSecret` to authenticate. + +```csharp +var options = new RestClientOptions("https://api.twitter.com") { + Authenticator = OAuth1Authenticator.ForRequestToken(consumerKey, consumerSecret) +}; +var client = new RestClient(options); +var request = new RestRequest("oauth/request_token"); +``` + +The response should contain the token and the token secret, which can then be used to complete the authorization process. +If you need to provide the callback URL, assign the `CallbackUrl` property of the authenticator to the callback destination. + +### Access token + +Getting an access token is the usual third step in the 3-legged OAuth1 flow. +This method retrieves an access token when provided `consumerKey`, `consumerSecret`, `oauthToken`, and `oauthTokenSecret`. +If you don't have a token for this call, you need to make a call to get the request token as described above. + +```csharp +var authenticator = OAuth1Authenticator.ForAccessToken( + consumerKey, consumerSecret, oauthToken, oauthTokenSecret +); +var options = new RestClientOptions("https://api.twitter.com") { + Authenticator = authenticator +}; +var client = new RestClient(options); +var request = new RestRequest("oauth/access_token"); +``` + +If the second step in 3-leg OAuth1 flow returned a verifier value, you can use another overload of `ForAccessToken`: + +```csharp +var authenticator = OAuth1Authenticator.ForAccessToken( + consumerKey, consumerSecret, oauthToken, oauthTokenSecret, verifier +); +``` + +The response should contain the access token that can be used to make calls to protected resources. + +For refreshing access tokens, use one of the two overloads of `ForAccessToken` that accept `sessionHandle`. + +### Protected resource + +When the access token is available, use `ForProtectedResource` function to get the authenticator for accessing protected resources. + +```csharp +var authenticator = OAuth1Authenticator.ForAccessToken( + consumerKey, consumerSecret, accessToken, accessTokenSecret +); +var options = new RestClientOptions("https://api.twitter.com/1.1") { + Authenticator = authenticator +}; +var client = new RestClient(options); +var request = new RestRequest("statuses/update.json", Method.Post) + .AddParameter("status", "Hello Ladies + Gentlemen, a signed OAuth request!") + .AddParameter("include_entities", "true"); +``` + +### xAuth + +xAuth is a simplified version of OAuth1. It allows sending the username and password as `x_auth_username` and `x_auth_password` request parameters and directly get the access token. xAuth is not widely supported, but RestSharp still allows using it. + +Create an xAuth authenticator using `OAuth1Authenticator.ForClientAuthentication` function: + +```csharp +var authenticator = OAuth1Authenticator.ForClientAuthentication( + consumerKey, consumerSecret, username, password +); +``` + +### 0-legged OAuth + +The access token authenticator can be used in 0-legged OAuth scenarios by providing `null` for the `consumerSecret`. + +```csharp +var authenticator = OAuth1Authenticator.ForAccessToken( + consumerKey, null, oauthToken, oauthTokenSecret +); +``` + +## OAuth2 + +RestSharp has two very simple authenticators to send the access token as part of the request. + +`OAuth2UriQueryParameterAuthenticator` accepts the access token as the only constructor argument, and it will send the provided token as a query parameter `oauth_token`. + +`OAuth2AuthorizationRequestHeaderAuthenticator` has two constructors. One only accepts a single argument, which is the access token. The other constructor also allows you to specify the token type. The authenticator will then add an `Authorization` header using the specified token type or `OAuth` as the default token type, and the token itself. + +For example: + +```csharp +var authenticator = new OAuth2AuthorizationRequestHeaderAuthenticator( + token, "Bearer" +); +var options = new RestClientOptions("https://example.com") { + Authenticator = authenticator +}; +var client = new RestClient(options); +``` + +The code above will tell RestSharp to send the bearer token with each request as a header. Essentially, the code above does the same as the sample for `JwtAuthenticator` below. + +As those authenticators don't do much to get the token itself, you might be interested in looking at our [sample OAuth2 authenticator](../usage/example.md#authenticator), which requests the token on its own. + +## JWT + +The JWT authentication can be supported by using `JwtAuthenticator`. It is a very simple class that can be constructed like this: + +```csharp +var authenticator = new JwtAuthenticator(myToken); +var options = new RestClientOptions("https://example.com") { + Authenticator = authenticator +}; +var client = new RestClient(options); +``` + +For each request, it will add an `Authorization` header with the value `Bearer `. + +As you might need to refresh the token from, you can use the `SetBearerToken` method to update the token. + +## Custom authenticator + +You can write your own implementation by implementing `IAuthenticator` and +registering it with your RestClient: + +```csharp +var authenticator = new SuperAuthenticator(); // implements IAuthenticator +var options = new RestClientOptions("https://example.com") { + Authenticator = authenticator +}; +var client = new RestClient(options); +``` + +The `Authenticate` method is the very first thing called upon calling `RestClient.Execute` or `RestClient.Execute`. +It gets the `RestRequest` currently being executed giving you access to every part of the request data (headers, parameters, etc.) + +You can find an example of a custom authenticator that fetches and uses an OAuth2 bearer token [here](../usage/example.md#authenticator). diff --git a/docs/versioned_docs/version-v110/advanced/configuration.md b/docs/versioned_docs/version-v110/advanced/configuration.md new file mode 100644 index 000000000..95d506fc8 --- /dev/null +++ b/docs/versioned_docs/version-v110/advanced/configuration.md @@ -0,0 +1,230 @@ +--- +title: Configuration +description: Learn how to configure RestClient for non-trivial use cases. +sidebar_position: 1 +--- + +# Configuring RestClient + +This page describes how to create and configure `RestClient`. + +## Basic configuration + +The primary `RestClient` constructor accepts an instance of `RestClientOptions`. Most of the time, default option values don't need to be changed. However, in some cases, you'd want to configure the client differently, so you'd need to change some of the options in your code. The constructor also contains a few optional parameters for additional configuration that is not covered by client options. Here's the constructor signature: + +```csharp +public RestClient( + RestClientOptions options, + ConfigureHeaders? configureDefaultHeaders = null, + ConfigureSerialization? configureSerialization = null, + bool useClientFactory = false +) +``` + +Constructor parameters are: + +| Name | Description | Mandatory | +|-------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------| +| options | Client options | Yes | +| configureDefaultHeaders | Function to configure headers. Allows to configure default headers for `HttpClient`. Most of the time you'd prefer using `client.AddDefaultHeader` instead. | No | +| configureSerialization | Function to configure client serializers with non-default options or to use a different serializer ([learn more](serialization.md)) | No | +| useClientFactory | Instructs the client to use `SimpleFactory` ([learn more](../usage/usage.md#simple-factory)) to get an `HttpClient` instance | No | + +Here's an example of how to create a client using client options: + +```csharp +var options = new RestClientOptions("https://localhost:5000/api") { + DisableCharset = true +}; +var client = new RestClient(options); +``` + +When you only need to set the base URL, you can use a simplified constructor: + +```csharp +var client = new RestClient("https://localhost:5000/api"); +``` + +The simplified constructor will create an instance of client options and set the base URL provided as the constructor argument. + +Finally, you can override properties of default options using a configuration function. Here's the constructor signature that supports this method: + +```csharp +public RestClient( + ConfigureRestClient? configureRestClient = null, + ConfigureHeaders? configureDefaultHeaders = null, + ConfigureSerialization? configureSerialization = null, + bool useClientFactory = false +) +``` + +For example: + +```csharp +var client = new RestClient(options => { + options.BaseUrl = new Url("https://localhost:5000/api"), + options.DisableCharset = true +}); +``` + +You can also provide the base URL as a constructor argument like this: + +```csharp +var client = new RestClient("https://localhost:5000/api", options => { + options.DisableCharset = true +}); +``` + +## Using custom HttpClient + +By default, RestSharp creates an instance of `HttpClient` configured using the client options, and keeps it during the lifetime of the client. When the `RestClient` instance gets disposed, it also disposes the `HttpClient` instance. + +There might be a case when you need to provide your own `HttpClient`. For example, you would want to use `HttpClient` created by HTTP client factory. RestSharp allows you to do it by using additional constructors. These constructors are: + +```csharp +// Create a client using an existing HttpClient and RestClientOptions (optional) +public RestClient( + HttpClient httpClient, + RestClientOptions? options, + bool disposeHttpClient = false, + ConfigureSerialization? configureSerialization = null +) + +// Create a client using an existing HttpClient and optional RestClient configuration function +public RestClient( + HttpClient httpClient, + bool disposeHttpClient = false, + ConfigureRestClient? configureRestClient = null, + ConfigureSerialization? configureSerialization = null +) +``` + +The `disposeHttpClient` argument tells the client to dispose `HttpClient` when the client itself gets disposed. It's set to `false` by default as when the `HttpClient` is provided from the outside, it should normally be disposed on the outside as well. + +## Using custom message handler + +Unless you use an external instance of `HttpClient`, the `RestClient` creates one when being constructed, and it will use the default HTTP message handler, configured using `RestClientOptions`. Normally, you'd get a `SocketHttpHandler` with modern .NET, and `WinHttpHandler` with .NET Framework. + +There might be a case when you need to configure the HTTP message handler. For example, you want to add a delegating message handler. RestSharp allows you to do it by using additional constructors. There's one constructor that allows you to pass the custom `HttpMessageHandler`: + +```csharp +public RestClient( + HttpMessageHandler handler, + bool disposeHandler = true, + ConfigureRestClient? configureRestClient = null, + ConfigureSerialization? configureSerialization = null +) +``` + +This constructor will create a new `HttpClient` instance using the provided message handler. As RestSharp will dispose the `HttpClient` instance when the `RestClient` instance gets disposed, the handler will be disposed as well. If you want to change that and keep the handler, set the `disposeHandler` parameter to `false`. + +:::note +When using a custom message handler, RestSharp **will not** configure it with client options, which are normally used to configure the handler created by RestSharp. +::: + +Another way to customize the message handler is to allow RestSharp to create a handler, but then configure it, or wrap it in a delegating handler. It can be done by using the `RestClientOptions.ConfigureMessageHandler` property. It can be set to a function that receives the handler created by RestSharp and returned either the same handler with different settings, or a new handler. + +For example, if you want to use `MockHttp` and its handler for testing, you can do it like this: + +```csharp +var mockHttp = new MockHttpMessageHandler(); +// Configure the MockHttp handler to do the checks +... + +var options = new RestClientOptions(Url) { + ConfigureMessageHandler = _ => mockHttp +}; +using var client = new RestClient(options); +``` + +In this example, we are reassigning the handler to MockHttp, so the handler created by RestSharp isn't used. In other cases you want to use delegating handlers as middleware, so you'd pass the handler created by RestSharp to the delegating handler: + +```csharp +var options = new RestClientOptions(Url) { + ConfigureMessageHandler = handler => new MyDelegatingHandler(handler) +}; +using var client = new RestClient(options); +``` + +## Client options + +RestSharp allows configuring `RestClient` using client options, as mentioned at the beginning of this page. Below, you find more details about available options. + +| Option | Description | +|----------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `BaseUrl` | Client base URL. It can also be provided as the `RestClientOptions` constructor argument. | +| `ConfigureMessageHandler` | Configures the HTTP message handler (see above). | +| `CalculateResponseStatus` | Function to calculate a different response status from `HttpResponseMessage`. By default, the request is considered as complete if it returns a successful status code or 404. | +| `Authenticator` | Client-level authenticator. Read more about authenticators [here](authenticators.md). | +| `Interceptors` | A collector of interceptors. Read more about interceptors [here](interceptors.md). | +| `Credentials` | Instance of `ICredentials` used for NTLM or Kerberos authentication. Not supported in browsers. | +| `UseDefaultCredentials` | Whether to use default OS credentials for NTLM or Kerberos authentication. Not supported in browsers. | +| `DisableCharset` | When set to `true`, the `Content-Type` header won't have the `charset` portion. Some older web servers don't understand the `charset` portion in the header and fail to process the request. | +| `AutomaticDecompression` | Allows customizing supported decompression methods. Default is `All` except for .NET Framework that only support `GZip`. Not supported in browsers. | +| `MaxRedirects` | The number of redirects to follow. Not supported in browsers. | +| `ClientCertificates` | A collection of X.509 client certificates to be used for authentication. Not supported in browsers. | +| `Proxy` | Can be used if the client needs to use an explicit, non-default proxy. Not supported in browsers, on iOS and tvOS. | +| `CachePolicy` | Shortcut for setting the default value for `Cache-Control` header. | +| `FollowRedirects` | Instructs the client to follow redirects. Default is `true`. | +| `Expect100Continue` | Gets or sets a value that indicates if the `Expect` header for an HTTP request contains `Continue`. | +| `UserAgent` | Allows overriding the default value for `User-Agent` header, which is `RestSharp/{version}`. | +| `PreAuthenticate` | Gets or sets a value that indicates whether the client sends an `Authorization` header with the request. Not supported in browsers. | +| `RemoteCertificateValidationCallback` | Custom function to validate the server certificate. Normally, it's used when the server uses a certificate that isn't trusted by default. | +| `BaseHost` | Value for the `Host` header sent with each request. | +| `CookieContainer` | Custom cookie container that will be shared among all calls made by the client. Normally not required as RestSharp handles cookies without using a client-level cookie container. | +| `MaxTimeout` | Client-level timeout in milliseconds. If the request timeout is also set, this value isn't used. | +| `Encoding` | Default request encoding. Override it only if you don't use UTF-8. | +| `ThrowOnDeserializationError` | Forces the client to throw if it fails to deserialize the response. Remember that not all deserialization issues forces the serializer to throw. Default is `false`, so the client will return a `RestResponse` with deserialization exception details. Only relevant for `Execute...` functions. | +| `FailOnDeserializationError` | When set to `true`, if the client fails to deserialize the response, the response object will have status `Failed`, although the HTTP calls might have been successful. Default is `true`. | +| `ThrowOnAnyError` | When set to `true`, the client will re-throw any exception from `HttpClient`. Default is `false`. Only applies for `Execute...` functions. | +| `AllowMultipleDefaultParametersWithSameName` | By default, adding parameters with the same name is not allowed. You can override this behaviour by setting this property to `true`. | +| `Encode` | A function to encode URLs, the default is a custom RestSharp function based on `Uri.EscapeDataString()`. Set it if you need a different way to do the encoding. | +| `EncodeQuery` | A function to encode URL query parameters. The default is the same function as for `Encode` property. | + +Some of the options are used by RestSharp code, but some are only used to configure the `HttpMessageHandler`. These options are: +- `Credentials` +- `UseDefaultCredentials` +- `AutomaticDecompression` +- `PreAuthenticate` +- `MaxRedirects` +- `RemoteCertificateValidationCallback` +- `ClientCertificates` +- `FollowRedirects` +- `Proxy` + +:::note +If setting these options to non-default values produce no desirable effect, check if your framework and platform supports them. RestSharp doesn't change behaviour based on values of those options. +::: + +The `IRestClient` interface exposes the `Options` property, so any option can be inspected at runtime. However, RestSharp converts the options object provided to the client constructor to an immutable object. Therefore, no client option can be changed after the client is instantiated. It's because changing client options at runtime can produce issues in concurrent environments, effectively rendering the client as not thread-safe. Apart from that, changing the options that are used to create the message handler would require re-creating the handler, and also `HttpClient`, which should not be done at runtime. + +## Configuring requests + +Client options apply to all requests made by the client. Sometimes, you want to fine-tune particular requests, so they execute with custom configuration. It's possible to do using properties of `RestRequest`, described below. + +| Name | Description | +|------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `AlwaysMultipartFormData` | When set to `true`, the request will be sent as a multipart form, even though it's not required. By default, RestSharp only sends requests with multiple attachments as multipart forms. Default is `false`. | +| `AlwaysSingleFileAsContent` | When set to true, the request with file attachment will not be sent as a multipart form, but as plain content. Default is `false`. It cannot be set to `true` when `AlwaysMultipartFormData` is set to `true`, or when the request has `POST` parameters. | +| `MultipartFormQuoteBoundary` | Default is `true`, which means that the form boundary string will be wrapped in quotes. If the server has an issue with that, setting this to `false` will remove quotes around the boundary. | +| `FormBoundary` | Allows specifying a custom multipart form boundary instead of using the default random string. | +| `RequestParameters` | Collection of request parameters. Normally, you won't need to use it as parameters are added to the request using `Add...` functions. | +| `CookieContainer` | Custom request-level cookie container. Default is `null`. You can still set request cookies using `AddCookie` and get response cookies from the response object without using cooking container. | +| `Authenticator` | Overrides the client-level authenticator. | +| `Files` | Collection of file parameters, read-only. Use `AddFile` for adding files to the request. | +| `Method` | Request HTTP method, default is `GET`. Only needed when using `Execute` or `ExecuteAsync` as other functions like `ExecutePostAsync` will override the request method. | +| `TImeout` | Overrides the client-level timeout. | +| `Resource` | Resource part of the remote endpoint URL. For example, when using the client-level base URL `https://localhost:5000/api` and `Resource` set to `weather`, the request will be sent to `https://localhost:5000/api/weather`. It can container resource placeholders to be used in combination with `AddUrlSegment` | +| `RequestFormat` | Identifies the request as JSON, XML, binary, or none. Rarely used because the client will set the request format based on the body type if functions like `AddJsonBody` or `AddXmlBody` are used. | +| `RootElement` | Used by the default deserializers to determine where to start deserializing from. Only supported for XML responses. Does not apply to requests. | +| `OnBeforeDeserialization` | **Obsolete** A function to be called before the response is deserializer. Allows changing the content before calling the deserializer. Use [interceptors](interceptors.md) instead. | +| `OnBeforeRequest` | **Obsolete** A function to be called right before the request is executed by `HttpClient`. It receives an instance of `HttpRequestMessage`. Use [interceptors](interceptors.md) instead. | +| `OnAfterRequest` | **Obsolete** A function to be called right after the request is executed by `HttpClient`. It receives an instance of `HttpResponseMessage`. Use [interceptors](interceptors.md) instead. | +| `Attempts` | When the request is being resent to retry, the property value increases by one. | +| `CompletionOption` | Instructs the client on when it should consider the request to be completed. The default is `ResponseContentRead`. It is automatically changed to `ResponseHeadersRead` when using async download functions or streaming. | +| `CachePolicy` | Overrides the client cache policy. | +| `ResponseWriter` | Allows custom handling of the response stream. The function gets the raw response stream and returns another stream or `null`. Cannot be used in combination with `AdvancedResponseWriter`. | +| `AdvancedResponseWriter` | Allows custom handling of the response. The function gets an instance of `HttpResponseMessage` and an instance of `RestRequest`. It must return an instance of `RestResponse`, so it effectively overrides RestSharp default functionality for creating responses. | +| `Interceptors` | Allows adding interceptors to the request. Both client-level and request-level interceptors will be called. | + +The table below contains all configuration properties of `RestRequest`. To learn more about adding request parameters, check the [usage page](../usage/usage.md#create-a-request) section about creating requests with parameters. diff --git a/docs/versioned_docs/version-v110/advanced/error-handling.md b/docs/versioned_docs/version-v110/advanced/error-handling.md new file mode 100644 index 000000000..7b3be11fc --- /dev/null +++ b/docs/versioned_docs/version-v110/advanced/error-handling.md @@ -0,0 +1,67 @@ +# Error handling + +If there is a network transport error (network is down, failed DNS lookup, etc), or any kind of server error (except 404), `RestResponse.ResponseStatus` will be set to `ResponseStatus.Error`, otherwise it will be `ResponseStatus.Completed`. + +If an API returns a 404, `ResponseStatus` will still be `Completed`. If you need access to the HTTP status code returned you will find it at `RestResponse.StatusCode`. +The `Status` property is an indicator of completion independent of the API error handling. + +Normally, RestSharp doesn't throw an exception if the request fails. + +However, it is possible to configure RestSharp to throw in different situations, when it normally doesn't throw +in favour of giving you the error as a property. + +| Property | Behavior | +|-------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `FailOnDeserializationError` | Changes the default behavior when failed deserialization results in a successful response with an empty `Data` property of the response. Setting this property to `true` will tell RestSharp to consider failed deserialization as an error and set the `ResponseStatus` to `Error` accordingly. | +| `ThrowOnDeserializationError` | Changes the default behavior when failed deserialization results in empty `Data` property of the response. Setting this property to `true` will tell RestSharp to throw when deserialization fails. | +| `ThrowOnAnyError` | Setting this property to `true` changes the default behavior and forces RestSharp to throw if any errors occurs when making a request or during deserialization. | + +Those properties are available for the `RestClientOptions` and will be used for all request made with the client instance. + +For example, you can configure the client to throw an exception if any error occurs when making a request, or when a request returns a non-successful HTTP status code: + +```csharp +var options = new RestClientOptions(url) { + ThrowOnAnyError = true +}; +var client = new RestClient(options); +var request = new RestRequest("resource/{id}").AddUrlSegment("id", 123); +// 👇 will throw if the request fails +var response = await client.ExecuteGetAsync(request); +``` + +:::warning +Please be aware that deserialization failures will only work if the serializer throws an exception when deserializing the response. +Many serializers don't throw by default, and just return a `null` result. RestSharp is unable to figure out why `null` is returned, so it won't fail in this case. +Check the serializer documentation to find out if it can be configured to throw on deserialization error. +::: + +There are also slight differences on how different overloads handle exceptions. + +Asynchronous generic methods `GetAsync`, `PostAsync` and so on, which aren't a part of `RestClient` interface (those methods are extension methods) return `Task`. It means that there's no `RestResponse` to set the response status to error. We decided to throw an exception when such a request fails. It is a trade-off between the API consistency and usability of the library. Usually, you only need the content of `RestResponse` instance to diagnose issues and most of the time the exception would tell you what's wrong. + +Below, you can find how different extensions deal with errors. Note that functions, which don't throw by default, will throw exceptions when `ThrowOnAnyError` is set to `true`. + +| Function | Throws on errors | +|:----------------------|:-----------------| +| `ExecuteAsync` | No | +| `ExecuteGetAsync` | No | +| `ExecuteGetAsync` | No | +| `ExecutePostAsync` | No | +| `ExecutePostAsync` | No | +| `ExecutePutAsync` | No | +| `ExecutePutAsync` | No | +| `GetAsync` | Yes | +| `GetAsync` | Yes | +| `PostAsync` | Yes | +| `PostAsync` | Yes | +| `PatchAsync` | Yes | +| `PatchAsync` | Yes | +| `DeleteAsync` | Yes | +| `DeleteAsync` | Yes | +| `OptionsAsync` | Yes | +| `OptionsAsync` | Yes | +| `HeadAsync` | Yes | +| `HeadAsync` | Yes | + +In addition, all the functions for JSON requests, like `GetJsonAsync` and `PostJsonAsync` throw an exception if the HTTP call fails. diff --git a/docs/versioned_docs/version-v110/advanced/interceptors.md b/docs/versioned_docs/version-v110/advanced/interceptors.md new file mode 100644 index 000000000..a3bf7a9bc --- /dev/null +++ b/docs/versioned_docs/version-v110/advanced/interceptors.md @@ -0,0 +1,84 @@ +--- +title: Interceptors +--- + +## Intercepting requests and responses + +Interceptors are a powerful feature of RestSharp that allows you to modify requests and responses before they are sent or received. You can use interceptors to add headers, modify the request body, or even cancel the request. You can also use interceptors to modify the response before it is returned to the caller. + +### Implementing an interceptor + +To implement an interceptor, you need to create a class that inherits the `Interceptor` base class. The base class implements all interceptor methods as virtual, so you can override them in your derived class. + +Methods that you can override are: +- `BeforeRequest(RestRequest request, CancellationToken cancellationToken)` +- `AfterRequest(RestResponse response, CancellationToken cancellationToken)` +- `BeforeHttpRequest(HttpRequestMessage requestMessage, CancellationToken cancellationToken)` +- `AfterHttpRequest(HttpResponseMessage responseMessage, CancellationToken cancellationToken)` +- `BeforeDeserialization(RestResponse response, CancellationToken cancellationToken)` + +All those functions must return a `ValueTask` instance. + +Here's an example of an interceptor that adds a header to a request: + +```csharp +// This interceptor adds a header to the request +// You'd not normally use this interceptor, as RestSharp already has a method +// to add headers to the request +class HeaderInterceptor(string headerName, string headerValue) : Interceptors.Interceptor { + public override ValueTask BeforeHttpRequest(HttpRequestMessage requestMessage, CancellationToken cancellationToken) { + requestMessage.Headers.Add(headerName, headerValue); + return ValueTask.CompletedTask; + } +} +``` + +Because interceptor functions return `ValueTask`, you can use `async` and `await` inside them. + +### Using an interceptor + +It's possible to add as many interceptors as you want, both to the client and to the request. The interceptors are executed in the order they were added. + +Adding interceptors to the client is done via the client options: + +```csharp +var options = new RestClientOptions("https://api.example.com") { + Interceptors = [new HeaderInterceptor("Authorization", token)] +}; +var client = new RestClient(options); +``` + +When you add an interceptor to the client, it will be executed for every request made by that client. + +You can also add an interceptor to a specific request: + +```csharp +var request = new RestRequest("resource") { + Interceptors = [new HeaderInterceptor("Authorization", token)] +}; +``` + +In this case, the interceptor will only be executed for that specific request. + +### Deprecation notice + +Interceptors aim to replace the existing request hooks available in RestSharp prior to version 111.0. Those hooks are marked with `Obsolete` attribute and will be removed in the future. If you are using those hooks, we recommend migrating to interceptors as soon as possible. + +To make the migration easier, RestSharp provides a class called `CompatibilityInterceptor`. It has properties for the hooks available in RestSharp 110.0 and earlier. You can use it to migrate your code to interceptors without changing the existing logic. + +For example, a code that uses `OnBeforeRequest` hook: + +```csharp +var request = new RestRequest("success"); +request.OnBeforeDeserialization += _ => throw new Exception(exceptionMessage); +``` + +Can be migrated to interceptors like this: + +```csharp +var request = new RestRequest("success") { + Interceptors = [new CompatibilityInterceptor { + OnBeforeDeserialization = _ => throw new Exception(exceptionMessage) + }] +}; +``` \ No newline at end of file diff --git a/docs/versioned_docs/version-v110/advanced/serialization.md b/docs/versioned_docs/version-v110/advanced/serialization.md new file mode 100644 index 000000000..b7092519a --- /dev/null +++ b/docs/versioned_docs/version-v110/advanced/serialization.md @@ -0,0 +1,148 @@ +# Serialization + +One of the most common reasons to choose RestSharp over plain `HttpClient` is its rich build-in serialization support. RestSharp allows adding complex objects as request body to be serialized when making a call to an API endpoint, and deserializing the response to a given .NET type. RestSharp supports JSON and XML serialization and deserialization by default. In addition, you can use a CSV serializer or write your own. + +In contrast to `System.Net.Http.Json` package that contains `HttpClient` extensions to make `GET` or `POST` calls using JSON, RestSharp support JSON responses for all HTTP methods, not just for `GET`. + +## Configuration + +:::tip +The default behavior of RestSharp is to swallow deserialization errors and return `null` in the `Data` +property of the response. Read more about it in the [Error Handling](error-handling.md). +::: + +You can tell RestSharp to use a custom serializer by using the `configureSerialization` constructor parameter: + +```csharp +var client = new RestClient( + options, + configureSerialization: s => s.UseSerializer(() => new CustomSerializer()); +); +``` + +All RestSharp serializers implement the `IRestSerializer` interface. Among other things, the interface requires implementing the `AcceptedContentTypes` property, which must return a collection of content types supported by the serializer. Being configured to use certain serializers, RestSharp populates the `Accept` header accordingly, so it doesn't need to be set manually. + +When making a call, RestSharp sets the request content type according to the request body type. For example, when you use `AddJsonBody`, the content type is set to `application/json`. Normally, you won't need to set the `Content-Type` header manually. If you need to set a custom content type for a JSON call, you can use the optional `contentType` argument of `AddJsonBody`, for example: + +```csharp +request.AddJsonBody(data, "text/json"); +``` + +## JSON + +The default JSON serializer uses `System.Text.Json`, which is a part of .NET since .NET 6. For earlier versions, it is added as a dependency. There are also a few serializers provided as additional packages. + +By default, RestSharp will use `JsonSerializerDefaults.Web` configuration. If necessary, you can specify your own options: + +```csharp +var client = new RestClient( + options, + configureSerialization: s => s.UseSystemTextJson(new JsonSerializerOptions {...}) +); +``` + +## XML + +The default XML serializer is `DotNetXmlSerializer`, which uses `System.Xml.Serialization` library from .NET. + +In previous versions of RestSharp, the default XML serializer was a custom RestSharp XML serializer. To make the code library size smaller, that serializer is now available as a separate package [`RestSharp.Serializers.Xml`](https://www.nuget.org/packages/RestSharp.Serializers.Xml). +You can add it back if necessary by installing the package and adding it to the client: + +```csharp +var client = new RestClient( + options, + configureSerialization: s => s.UseXmlSerializer() +); +``` + +As before, you can supply three optional arguments for a custom namespace, custom root element, and if you want to use `SerializeAs` and `DeserializeAs` attributed. + +## NewtonsoftJson (aka Json.Net) + +The `NewtonsoftJson` package is the most popular JSON serializer for .NET. It handles all possible scenarios and is very configurable. Such a flexibility comes with the cost of performance. If you need speed, keep the default JSON serializer. + +RestSharp support Json.Net serializer via a separate package [`RestSharp.Serializers.NewtonsoftJson`](https://www.nuget.org/packages/RestSharp.Serializers.NewtonsoftJson). + +:::warning +Please note that `RestSharp.Newtonsoft.Json` package is not provided by RestSharp, is marked as obsolete on NuGet, and no longer supported by its creator. +::: + +Use the extension method provided by the package to configure the client: + +```csharp +var client = new RestClient( + options, + configureSerialization: s => s.UseNewtonsoftJson() +); +``` + +The serializer configures some options by default: + +```csharp +JsonSerializerSettings DefaultSettings = new JsonSerializerSettings { + ContractResolver = new CamelCasePropertyNamesContractResolver(), + DefaultValueHandling = DefaultValueHandling.Include, + TypeNameHandling = TypeNameHandling.None, + NullValueHandling = NullValueHandling.Ignore, + Formatting = Formatting.None, + ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor +}; +``` + +If you need to use different settings, you can supply your instance of +`JsonSerializerSettings` as a parameter for the extension method. + +## CSV + +A separate package `RestSharp.Serializers.CsvHelper` provides a CSV serializer for RestSharp. It is based on the +`CsvHelper` library. + +Use the extension method provided by the package to configure the client: + +```csharp +var client = new RestClient( + options, + configureSerialization: s => s.UseCsvHelper() +); +``` + +You can also supply your instance of `CsvConfiguration` as a parameter for the extension method. + +```csharp +var client = new RestClient( + options, + configureSerialization: s => s.UseCsvHelper( + new CsvConfiguration(CultureInfo.InvariantCulture) {...} + ) +); +``` + +## Custom + +You can also implement your custom serializer. To support both serialization and +deserialization, you must implement the `IRestSerializer` interface. + +Here is an example of a custom serializer that uses `System.Text.Json`: + +```csharp +public class SimpleJsonSerializer : IRestSerializer { + public string? Serialize(object? obj) => obj == null ? null : JsonSerializer.Serialize(obj); + + public string? Serialize(Parameter bodyParameter) => Serialize(bodyParameter.Value); + + public T? Deserialize(RestResponse response) => JsonSerializer.Deserialize(response.Content!); + + public ContentType ContentType { get; set; } = ContentType.Json; + + public ISerializer Serializer => this; + public IDeserializer Deserializer => this; + public DataFormat DataFormat => DataFormat.Json; + public string[] AcceptedContentTypes => ContentType.JsonAccept; + public SupportsContentType SupportsContentType + => contentType => contentType.Value.EndsWith("json", StringComparison.InvariantCultureIgnoreCase); +} +``` + +The `SupportedContentTypes` function will be used to check if the serializer is able to deserialize the response based on the `Content-Type` response header. + +The `ContentType` property will be used when making a request so the server knows how to handle the payload. diff --git a/docs/versioned_docs/version-v110/changelog.md b/docs/versioned_docs/version-v110/changelog.md new file mode 100644 index 000000000..c19657872 --- /dev/null +++ b/docs/versioned_docs/version-v110/changelog.md @@ -0,0 +1,24 @@ +--- +title: What's new +description: List of changes for the current major version +sidebar_position: 1 +--- + +# Changelog + +For release notes of previous versions, please check the [Releases page](https://github.com/restsharp/RestSharp/releases) in RestSharp GitHub repository. + +## What's Changed +* Added default parameters to the request. They got missing somehow. +* Consider the boundary quotes request option value. +* Made `BuildUrl` an extension so it can be used publicly. +* Added client-level cookie container. + +## Breaking change + +The `IRestClient` interface signature is different, so any non-standard implementations need to adopt the changes. + +To keep `DefaultParameters` thread-safe, it got a new type `DefaultParameters`, and request property `Parameters` has a dedicated type `RequestParameter`. Code-wise the change is non-breaking as the signatures are the same, but v110 is not binary compatible with previous versions. The difference is that `DefaultParameters` collection wraps all its mutations in a lock. + +**Full Changelog**: https://github.com/restsharp/RestSharp/compare/109.0.1...110.0.0 + diff --git a/docs/versioned_docs/version-v110/intro.md b/docs/versioned_docs/version-v110/intro.md new file mode 100644 index 000000000..caf619979 --- /dev/null +++ b/docs/versioned_docs/version-v110/intro.md @@ -0,0 +1,112 @@ +--- +sidebar_position: 2 +title: Quick start +--- + +## Introduction + +:::warning +RestSharp v107+ changes the library API surface and its behaviour significantly. We advise looking at [migration](/migration) docs to understand how to migrate to the latest version of RestSharp. +::: + +The main purpose of RestSharp is to make synchronous and asynchronous calls to remote resources over HTTP. As the name suggests, the main audience of RestSharp are developers who use REST APIs. However, RestSharp can call any API over HTTP, as long as you have the resource URI and request parameters that you want to send comply with W3C HTTP standards. + +One of the main challenges of using HTTP APIs for .NET developers is to work with requests and responses of different kinds and translate them to complex C# types. RestSharp can take care of serializing the request body to JSON or XML and deserialize the response. It can also form a valid request URI based on different parameter kinds: path, query, form or body. + +## Getting Started + +Before you can use RestSharp in your application, you need to add the NuGet package. You can do it using your IDE or the command line: + +``` +dotnet add package RestSharp +``` + +### Basic Usage + +If you only have a small number of one-off API requests to perform, you can use RestSharp like this: + +```csharp +using RestSharp; +using RestSharp.Authenticators; + +var options = new RestClientOptions("https://api.twitter.com/1.1") { + Authenticator = new HttpBasicAuthenticator("username", "password") +}; +var client = new RestClient(options); +var request = new RestRequest("statuses/home_timeline.json"); +// The cancellation token comes from the caller. You can still make a call without it. +var response = await client.GetAsync(request, cancellationToken); +``` + +It will return a `RestResponse` back, which contains all the information returned from the remote server. +You have access to the headers, content, HTTP status and more. + +You can also use generic overloads like `Get` to automatically deserialize the response into a .NET class. + +For example: + +```csharp +using RestSharp; +using RestSharp.Authenticators; + +var options = new RestClientOptions("https://api.twitter.com/1.1") { + Authenticator = new HttpBasicAuthenticator("username", "password") +}; +var client = new RestClient(options); + +var request = new RestRequest("statuses/home_timeline.json"); + +// The cancellation token comes from the caller. You can still make a call without it. +var timeline = await client.GetAsync(request, cancellationToken); +``` + +Both snippets above use the `GetAsync` extension, which is a wrapper about `ExecuteGetAsync`, which, in turn, is a wrapper around `ExecuteAsync`. +All `ExecuteAsync` overloads and return the `RestResponse` or `RestResponse`. + +The most important difference is that async methods named after HTTP methods (like `GetAsync` or `PostAsync`) return `Task` instead of `Task>`. It means that you won't get an error response if the request fails as those methods throw an exception for unsuccessful HTTP calls. For keeping the API consistent, non-generic functions like `GetAsync` or `PostAsync` also throw an exception if the request fails, although they return the `Task`. + +Read [here](advanced/error-handling.md) about how RestSharp handles exceptions. + +RestSharp also offers simple ways to call APIs that accept and return JSON payloads. You can use the `GetJsonAsync` and `PostJsonAsync` extension methods, which will automatically serialize the request body to JSON and deserialize the response to the specified type. + +```csharp +var client = new RestClient(options); +var timeline = await client.GetJsonAsync("statuses/home_timeline.json", cancellationToken); +``` + +Read [here](usage/usage.md#json-requests) about making JSON calls without preparing a request object. + +### Content type + +RestSharp supports sending XML or JSON body as part of the request. To add a body to the request, simply call `AddJsonBody` or `AddXmlBody` method of the `RestRequest` object. + +There is no need to set the `Content-Type` or add the `DataFormat` parameter to the request when using those methods, RestSharp will do it for you. + +RestSharp will also handle both XML and JSON responses and perform all necessary deserialization tasks, depending on the server response type. Therefore, you only need to add the `Accept` header if you want to deserialize the response manually. + +For example, only you'd only need these lines to make a request with JSON body: + +```csharp +var request = new RestRequest("address/update").AddJsonBody(updatedAddress); +var response = await client.PostAsync(request); +``` + +It's also possible to make the same call using `PostAsync` shorter syntax: + +```csharp +var response = await PostJsonAsync( + "address/update", request, cancellationToken +); +``` + +Read more about serialization and deserialization [here](advanced/serialization.md). + +### Response + +When you use `ExecuteAsync`, you get an instance of `RestResponse` back. The response object has the `Content` property, which contains the response as string. You can find other useful properties there, like `StatusCode`, `ContentType` and so on. If the request wasn't successful, you'd get a response back with `IsSuccessful` property set to `false` and the error explained in the `ErrorException` and `ErrorMessage` properties. + +When using typed `ExecuteAsync`, you get an instance of `RestResponse` back, which is identical to `RestResponse` but also contains the `T Data` property with the deserialized response. + +None of `ExecuteAsync` overloads throw if the remote server returns an error. You can inspect the response and find the status code, error message, and, potentially, an exception. + +Extensions like `GetAsync` will not return the whole `RestResponse` but just a deserialized response. These extensions will throw an exception if the remote server returns an error. The exception details contain the status code returned by the server. diff --git a/docs/versioned_docs/version-v110/usage/_category_.json b/docs/versioned_docs/version-v110/usage/_category_.json new file mode 100644 index 000000000..bd2045cd6 --- /dev/null +++ b/docs/versioned_docs/version-v110/usage/_category_.json @@ -0,0 +1,7 @@ +{ + "label": "Using RestSharp", + "position": 3, + "link": { + "type": "generated-index" + } +} diff --git a/docs/versioned_docs/version-v110/usage/example.md b/docs/versioned_docs/version-v110/usage/example.md new file mode 100644 index 000000000..458ed57bb --- /dev/null +++ b/docs/versioned_docs/version-v110/usage/example.md @@ -0,0 +1,148 @@ +# Example + +RestSharp works best as the foundation for a proxy class for your API. Each API would most probably require different settings for `RestClient`. Hence, a dedicated API class (and its interface) gives you sound isolation between different `RestClient` instances and make them testable. + +For example, let's look at a simple Twitter API v2 client, which uses OAuth2 machine-to-machine authentication. For it to work, you would need to have access to the Twitter Developers portal, a project, and an approved application inside the project with OAuth2 enabled. + +## Client model + +Before implementing an API client, we need to have a model for it. The model includes an abstraction for the client, which has functions for the API calls we are interested to implement. In addition, the client model would include the necessary request and response models. Usually those are simple classes or records without logic, which are often referred to as DTOs (data transfer objects). + +This example starts with a single function that retrieves one Twitter user. Lets being by defining the API client interface: + +```csharp +public interface ITwitterClient { + Task GetUser(string user); +} +``` + +As the function returns a `TwitterUser` instance, we need to define it as a model: + +```csharp +public record TwitterUser(string Id, string Name, string Username); +``` + +## Client implementation + +When that is done, we can implement the interface and add all the necessary code blocks to get a working API client. + +The client class needs the following: +- A constructor for passing API credentials +- A wrapped `RestClient` instance with the Twitter API base URI pre-configured +- An authenticator to support authorizing the client using Twitter OAuth2 authentication +- The actual function to get the user (to implement the `ITwitterClient` interface) + +Creating an authenticator is described [below](#authenticator). + +Here's how the client implementation could look like: + +```csharp +public class TwitterClient : ITwitterClient, IDisposable { + readonly RestClient _client; + + public TwitterClient(string apiKey, string apiKeySecret) { + var options = new RestClientOptions("https://api.twitter.com/2"); + _client = new RestClient(options); + } + + public async Task GetUser(string user) { + var response = await _client.GetJsonAsync>( + "users/by/username/{user}", + new { user } + ); + return response!.Data; + } + + record TwitterSingleObject(T Data); + + public void Dispose() { + _client?.Dispose(); + GC.SuppressFinalize(this); + } +} +``` + +It is also possible to use ASP.NET Core Options for configuring the client, instead of passing the credentials as strings. For example, we can add a class for Twitter client options, and use it in a constructor: + +```csharp +public class TwitterClientOptions(string ApiKey, string ApiSecret); + +public TwitterClient(IOptions options) { + var opt = new RestClientOptions("https://api.twitter.com/2"); + _client = new RestClient(options); +} +``` + +Then, you can register and configure the client using ASP.NET Core dependency injection container. + +Right now, the client won't really work as Twitter API requires authentication. It's covered in the next section. + +## Authenticator + +Before we can call the API itself, we need to get a bearer token. Twitter exposes an endpoint `https://api.twitter.com/oauth2/token`. As it follows the OAuth2 conventions, the code can be used to create an authenticator for some other vendors. + +First, we need a model for deserializing the token endpoint response. OAuth2 uses snake case for property naming, so we need to decorate model properties with `JsonPropertyName` attribute: + +```csharp +record TokenResponse { + [JsonPropertyName("token_type")] + public string TokenType { get; init; } + [JsonPropertyName("access_token")] + public string AccessToken { get; init; } +} +``` + +Next, we create the authenticator itself. It needs the API key and API key secret to call the token endpoint using basic HTTP authentication. In addition, we can extend the list of parameters with the base URL to convert it to a more generic OAuth2 authenticator. + +The easiest way to create an authenticator is to inherit from the `AuthenticatorBase` base class: + +```csharp +public class TwitterAuthenticator : AuthenticatorBase { + readonly string _baseUrl; + readonly string _clientId; + readonly string _clientSecret; + + public TwitterAuthenticator(string baseUrl, string clientId, string clientSecret) : base("") { + _baseUrl = baseUrl; + _clientId = clientId; + _clientSecret = clientSecret; + } + + protected override async ValueTask GetAuthenticationParameter(string accessToken) { + Token = string.IsNullOrEmpty(Token) ? await GetToken() : Token; + return new HeaderParameter(KnownHeaders.Authorization, Token); + } +} +``` + +During the first call made by the client using the authenticator, it will find out that the `Token` property is empty. It will then call the `GetToken` function to get the token once and reuse the token going forward. + +Now, we need to implement the `GetToken` function in the class: + +```csharp +async Task GetToken() { + var options = new RestClientOptions(_baseUrl){ + Authenticator = new HttpBasicAuthenticator(_clientId, _clientSecret), + }; + using var client = new RestClient(options); + + var request = new RestRequest("oauth2/token") + .AddParameter("grant_type", "client_credentials"); + var response = await client.PostAsync(request); + return $"{response!.TokenType} {response!.AccessToken}"; +} +``` + +As we need to make a call to the token endpoint, we need our own short-lived instance of `RestClient`. Unlike the actual Twitter client, it will use the `HttpBasicAuthenticator` to send the API key and secret as the username and password. The client then gets disposed as we only use it once. + +Here we add a POST parameter `grant_type` with `client_credentials` as its value. At the moment, it's the only supported value. + +The POST request will use the `application/x-www-form-urlencoded` content type by default. + +:::note +Sample code provided on this page is a production code. For example, the authenticator might produce undesired side effect when multiple requests are made at the same time when the token hasn't been obtained yet. It can be solved rather than simply using semaphores or synchronized invocation. +::: + +## Final words + +This page demonstrates how an API client can be implemented as a typed, configurable client with its own interface. Usage of the client in applications is not covered here as different application types and target frameworks have their own idiomatic ways to use HTTP clients. \ No newline at end of file diff --git a/docs/versioned_docs/version-v110/usage/usage.md b/docs/versioned_docs/version-v110/usage/usage.md new file mode 100644 index 000000000..7c580f096 --- /dev/null +++ b/docs/versioned_docs/version-v110/usage/usage.md @@ -0,0 +1,570 @@ +--- +sidebar_position: 2 +--- + +# RestSharp basics + +This page describes some of the essential properties and features of RestSharp. + +## What RestSharp does + +Essentially, RestSharp is a wrapper around `HttpClient` that allows you to do the following: +- Add default parameters of any kind (not just headers) to the client, once +- Add parameters of any kind to each request (query, URL segment, form, attachment, serialized body, header) in a straightforward way +- Serialize the payload to JSON or XML if necessary +- Set the correct content headers (content type, disposition, length, etc.) +- Handle the remote endpoint response +- Deserialize the response from JSON or XML if necessary + +## API client + +The best way to call an external HTTP API is to create a typed client, which encapsulates RestSharp calls and doesn't expose the `RestClient` instance in public. + +You can find an example of a Twitter API client on the [Example](example.md) page. + +## Creating the client + +A RestSharp client can be instantiated by one of the constructors: + +```csharp +// Creates a client with default options to call a given base URL +var client = new RestClient("https://localhost:5000"); + +// Creates a client using the options object +var options = new RestClientOptions("https://localhost:5000") { + MaxTimeout = 1000 +}; +var client = new RestClient(options); +``` + +### Simple factory + +Another way to create the client instance is to use a simple client factory. The factory will use the `BaseUrl` property of the client options to cache `HttpClient` instances. Every distinct base URL will get its own `HttpClient` instance. Other options don't affect the caching. Therefore, if you use different options for the same base URL, you'll get the same `HttpClient` instance, which will not be configured with the new options. Options that aren't applied _after_ the first client instance is created are: + +* `Credentials` +* `UseDefaultCredentials` +* `AutomaticDecompression` +* `PreAuthenticate` +* `FollowRedirects` +* `RemoteCertificateValidationCallback` +* `ClientCertificates` +* `MaxRedirects` +* `MaxTimeout` +* `UserAgent` +* `Expect100Continue` + +Constructor parameters to configure the `HttpMessageHandler` and default `HttpClient` headers configuration are also ignored for the cached instance as the factory only configures the handler once. + +You need to set the `useClientFactory` parameter to `true` in the `RestClient` constructor to enable the factory. + +```csharp +var client = new RestClient("https://api.twitter.com/2", true); +``` + +## Create a request + +Before making a request using `RestClient`, you need to create a request instance: + +```csharp +var request = new RestRequest(resource); // resource is the sub-path of the client base path +``` + +The default request type is `GET` and you can override it by setting the `Method` property. You can also set the method using the constructor overload: + +```csharp +var request = new RestRequest(resource, Method.Post); +``` + +After you've created a `RestRequest`, you can add parameters to it. Below, you can find all the parameter types supported by RestSharp. + +### Adding headers + +Adds the header parameter as an HTTP header that is sent along with the request. The header name is the parameter's name and the header value is the value. + +You can use one of the following request methods to add a header parameter: + +```csharp +AddHeader(string name, string value); +AddHeader(string name, T value); // value will be converted to string +AddOrUpdateHeader(string name, string value); // replaces the header if it already exists +``` + +For example: + +```csharp +var request = new RestRequest("/path").AddHeader("X-Key", someKey); +``` + +You can also add header parameters to the client, and they will be added to every request made by the client. This is useful for adding authentication headers, for example. + +```csharp +client.AddDefaultHeader(string name, string value); +``` + +::: warning Content-Type +RestSharp will use the correct content type by default. Avoid adding the `Content-Type` header manually to your requests unless you are absolutely sure it is required. You can add a custom content type to the [body parameter](#request-body) itself. +::: + +### Get or Post parameters + +The default RestSharp parameter type is `GetOrPostParameter`. You can add `GetOrPost` parameter to the request using the `AddParameter` function: + +```csharp +request + .AddParameter("name1", "value1") + .AddParameter("name2", "value2"); +``` + +`GetOrPost` behaves differently based on the HTTP method. If you execute a `GET` call, RestSharp will append the parameters to the URL in the form `url?name1=value1&name2=value2`. + +On a `POST` or `PUT` requests, it depends on whether you have files attached to a request. +If not, the parameters will be sent as the body of the request in the form `name1=value1&name2=value2`. Also, the request will be sent as `application/x-www-form-urlencoded`. + +In both cases, name and value will automatically be URL-encoded, unless specified otherwise: + +```csharp +request.AddParameter("name", "Væ üé", false); // don't encode the value +``` + +If you have files, RestSharp will send a `multipart/form-data` request. Your parameters will be part of this request in the form: + +``` +Content-Disposition: form-data; name="parameterName" + +ParameterValue +``` + +You can also add `GetOrPost` parameter as a default parameter to the client. This will add the parameter to every request made by the client. + +```csharp +client.AddDefaultParameter("foo", "bar"); +``` + +It will work the same way as request parameters, except that it will be added to every request. + +### Using AddObject + +You can avoid calling `AddParameter` multiple times if you collect all the parameters in an object, and then use `AddObject`. +For example, this code: + +```csharp +var params = new { + status = 1, + priority = "high", + ids = new [] { "123", "456" } +}; +request.AddObject(params); +``` + +is equivalent to: + +```csharp +request.AddParameter("status", 1); +request.AddParameter("priority", "high"); +request.AddParameter("ids", "123,456"); +``` + +Remember that `AddObject` only works if your properties have primitive types. It also works with collections of primitive types as shown above. + +If you need to override the property name or format, you can do it using the `RequestProperty` attribute. For example: + +```csharp +public class RequestModel { + // override the name and the format + [RequestProperty(Name = "from_date", Format = "d")] + public DateTime FromDate { get; set; } +} + +// add it to the request +request.AddObject(new RequestModel { FromDate = DateTime.Now }); +``` + +In this case, the request will get a GET or POST parameter named `from_date` and its value would be the current date in short date format. + +### Using AddObjectStatic + +Request function `AddObjectStatic(...)` allows using pre-compiled expressions for getting property values. Compared to `AddObject` that uses reflections for each call, `AddObjectStatic` caches functions to retrieve properties from an object of type `T`, so it works much faster. + +You can instruct `AddObjectStatic` to use custom parameter names and formats, as well as supply the list of properties than need to be used as parameters. The last option could be useful if the type `T` has properties that don't need to be sent with HTTP call. + +To use custom parameter name or format, use the `RequestProperty` attribute. For example: + +```csharp +class TestObject { + [RequestProperty(Name = "some_data")] + public string SomeData { get; set; } + + [RequestProperty(Format = "d")] + public DateTime SomeDate { get; set; } + + [RequestProperty(Name = "dates", Format = "d")] + public DateTime[] DatesArray { get; set; } + + public int Plain { get; set; } + public DateTime[] PlainArray { get; set; } +} +``` + +### URL segment parameter + +Unlike `GetOrPost`, URL segment parameter replaces placeholder values in the request URL: + +```csharp +var request = new RestRequest("health/{entity}/status") + .AddUrlSegment("entity", "s2"); +``` + +When the request executes, RestSharp will try to match any `{placeholder}` with a parameter of that name (without the `{}`) and replace it with the value. So the above code results in `health/s2/status` being the URL. + +You can also add `UrlSegment` parameter as a default parameter to the client. This will add the parameter to every request made by the client. + +```csharp +client.AddDefaultUrlSegment("foo", "bar"); +``` + +### Cookies + +You can add cookies to a request using the `AddCookie` method: + +```csharp +request.AddCookie("foo", "bar"); +``` + +RestSharp will add cookies from the request as cookie headers and then extract the matching cookies from the response. You can observe and extract response cookies using the `RestResponse.Cookies` properties, which has the `CookieCollection` type. + +However, the usage of a default URL segment parameter is questionable as you can just include the parameter value to the base URL of the client. There is, however, a `CookieContainer` instance on the request level. You can either assign the pre-populated container to `request.CookieContainer`, or let the container be created by the request when you call `AddCookie`. Still, the container is only used to extract all the cookies from it and create cookie headers for the request instead of using the container directly. It's because the cookie container is normally configured on the `HttpClientHandler` level and cookies are shared between requests made by the same client. In most of the cases this behaviour can be harmful. + +If your use case requires sharing cookies between requests made by the client instance, you can use the client-level `CookieContainer`, which you must provide as the options' property. You can add cookies to the container using the container API. No response cookies, however, would be auto-added to the container, but you can do it in code by getting cookies from the `Cookes` property of the response and adding them to the client-level container available via `IRestClient.Options.CookieContainer` property. + +### Request Body + +RestSharp supports multiple ways to add a request body: +- `AddJsonBody` for JSON payloads +- `AddXmlBody` for XML payloads +- `AddStringBody` for pre-serialized payloads + +We recommend using `AddJsonBody` or `AddXmlBody` methods instead of `AddParameter` with type `BodyParameter`. Those methods will set the proper request type and do the serialization work for you. + +When you make a `POST`, `PUT` or `PATCH` request and added `GetOrPost` [parameters](#get-or-post-parameters), RestSharp will send them as a URL-encoded form request body by default. When a request also has files, it will send a `multipart/form-data` request. You can also instruct RestSharp to send the body as `multipart/form-data` by setting the `AlwaysMultipartFormData` property to `true`. + +You can specify a custom body content type if necessary. The `contentType` argument is available in all the overloads that add a request body. + +It is not possible to add client-level default body parameters. + +#### String body + +If you have a pre-serialized payload like a JSON string, you can use `AddStringBody` to add it as a body parameter. You need to specify the content type, so the remote endpoint knows what to do with the request body. For example: + +```csharp +const json = "{ data: { foo: \"bar\" } }"; +request.AddStringBody(json, ContentType.Json); +``` + +#### JSON body + +When you call `AddJsonBody`, it does the following for you: + +- Instructs the RestClient to serialize the object parameter as JSON when making a request +- Sets the content type to `application/json` +- Sets the internal data type of the request body to `DataType.Json` + +Here is the example: + +```csharp +var param = new MyClass { IntData = 1, StringData = "test123" }; +request.AddJsonBody(param); +``` + +It is possible to override the default content type by supplying the `contentType` argument. For example: + +```csharp +request.AddJsonBody(param, "text/x-json"); +``` + +If you use a pre-serialized string with `AddJsonBody`, it will be sent as-is. The `AddJsonBody` will detect if the parameter is a string and will add it as a string body with JSON content type. +Essentially, it means that top-level strings won't be serialized as JSON when you use `AddJsonBody`. To overcome this issue, you can use an overload of `AddJsonBody`, which allows you to tell RestSharp to serialize the string as JSON: + +```csharp +const string payload = @" +""requestBody"": { + ""content"": { + ""application/json"": { + ""schema"": { + ""type"": ""string"" + } + } + } +},"; +request.AddJsonBody(payload, forceSerialize: true); // the string will be serialized +request.AddJsonBody(payload); // the string will NOT be serialized and will be sent as-is +``` + +#### XML body + +When you call `AddXmlBody`, it does the following for you: + +- Instructs the RestClient to serialize the object parameter as XML when making a request +- Sets the content type to `application/xml` +- Sets the internal data type of the request body to `DataType.Xml` + +:::warning +Do not send XML string to `AddXmlBody`; it won't work! +::: + +### Query string + +`QueryString` works like `GetOrPost`, except that it always appends the parameters to the url in the form `url?name1=value1&name2=value2`, regardless of the request method. + +Example: + +```csharp +var client = new RestClient("https://search.me"); +var request = new RestRequest("search") + .AddParameter("foo", "bar"); +var response = await client.GetAsync(request); +``` + +It will send a `GET` request to `https://search.me/search?foo=bar`. + +For `POST`-style requests you need to add the query string parameter explicitly: + +```csharp +request.AddQueryParameter("foo", "bar"); +``` + +In some cases, you might need to prevent RestSharp from encoding the query string parameter. +To do so, set the `encode` argument to `false` when adding the parameter: + +```csharp +request.AddQueryParameter("foo", "bar/fox", false); +``` + +You can also add a query string parameter as a default parameter to the client. This will add the parameter to every request made by the client. + +```csharp +client.AddDefaultQueryParameter("foo", "bar"); +``` + +The line above will result in all the requests made by that client instance to have `foo=bar` in the query string for all the requests made by that client. + +## Making a call + +Once you've added all the parameters to your `RestRequest`, you are ready to make a request. + +`RestClient` has a single function for this: + +```csharp +public async Task ExecuteAsync( + RestRequest request, + CancellationToken cancellationToken = default +) +``` + +You can also avoid setting the request method upfront and use one of the overloads: + +```csharp +Task ExecuteGetAsync(RestRequest request, CancellationToken cancellationToken) +Task ExecutePostAsync(RestRequest request, CancellationToken cancellationToken) +Task ExecutePutAsync(RestRequest request, CancellationToken cancellationToken) +``` + +When using any of those methods, you will get the response content as string in `response.Content`. + +RestSharp can deserialize the response for you. To use that feature, use one of the generic overloads: + +```csharp +Task> ExecuteAsync(RestRequest request, CancellationToken cancellationToken) +Task> ExecuteGetAsync(RestRequest request, CancellationToken cancellationToken) +Task> ExecutePostAsync(RestRequest request, CancellationToken cancellationToken) +Task> ExecutePutAsync(RestRequest request, CancellationToken cancellationToken) +``` + +All the overloads with names starting with `Execute` don't throw an exception if the server returns an error. Read more about it [here](../advanced/error-handling.md). + +If you just need a deserialized response, you can use one of the extensions: + +```csharp +Task GetAsync(RestRequest request, CancellationToken cancellationToken) +Task PostAsync(RestRequest request, CancellationToken cancellationToken) +Task PutAsync(RestRequest request, CancellationToken cancellationToken) +Task HeadAsync(RestRequest request, CancellationToken cancellationToken) +Task PatchAsync(RestRequest request, CancellationToken cancellationToken) +Task DeleteAsync(RestRequest request, CancellationToken cancellationToken) +``` + +Those extensions will throw an exception if the server returns an error, as there's no other way to float the error back to the caller. + +The `IRestClient` interface also has extensions for making requests without deserialization, which throw an exception if the server returns an error even if the client is configured to not throw exceptions. + +```csharp +Task GetAsync(RestRequest request, CancellationToken cancellationToken) +Task PostAsync(RestRequest request, CancellationToken cancellationToken) +Task PutAsync(RestRequest request, CancellationToken cancellationToken) +Task HeadAsync(RestRequest request, CancellationToken cancellationToken) +Task PatchAsync(RestRequest request, CancellationToken cancellationToken) +Task DeleteAsync(RestRequest request, CancellationToken cancellationToken) +``` + +### JSON requests + +To make a simple `GET` call and get a deserialized JSON response with a pre-formed resource string, use this: + +```csharp +var response = await client.GetJsonAsync("endpoint?foo=bar", cancellationToken); +``` + +You can also use a more advanced extension that uses an object to compose the resource string: + +```csharp +var client = new RestClient("https://example.org"); +var args = new { + id = "123", + foo = "bar" +}; +// Will make a call to https://example.org/endpoint/123?foo=bar +var response = await client.GetJsonAsync("endpoint/{id}", args, cancellationToken); +``` + +It will search for the URL segment parameters matching any of the object properties and replace them with values. All the other properties will be used as query parameters. + +Similar things are available for `POST` requests. + +```csharp +var request = new CreateOrder("123", "foo", 10100); +// Will post the request object as JSON to "orders" and returns a +// JSON response deserialized to OrderCreated +var result = client.PostJsonAsync("orders", request, cancellationToken); +``` + +```csharp +var request = new CreateOrder("123", "foo", 10100); +// Will post the request object as JSON to "orders" and returns a +// status code, not expecting any response body +var statusCode = client.PostJsonAsync("orders", request, cancellationToken); +``` + +The same two extensions also exist for `PUT` requests (`PutJsonAsync`); + +### JSON streaming APIs + +For HTTP API endpoints that stream the response data (like [Twitter search stream](https://developer.twitter.com/en/docs/twitter-api/tweets/filtered-stream/api-reference/get-tweets-search-stream)) you can use RestSharp with `StreamJsonAsync`, which returns an `IAsyncEnumerable`: + +```csharp +public async IAsyncEnumerable SearchStream( + [EnumeratorCancellation] CancellationToken cancellationToken = default +) { + var response = _client.StreamJsonAsync>( + "tweets/search/stream", cancellationToken + ); + + await foreach (var item in response.WithCancellation(cancellationToken)) { + yield return item.Data; + } +} +``` + +The main limitation of this function is that it expects each JSON object to be returned as a single line. It is unable to parse the response by combining multiple lines into a JSON string. + +### Uploading files + +To add a file to the request you can use the `RestRequest` function called `AddFile`. The main function accepts the `FileParameter` argument: + +```csharp +request.AddFile(fileParameter); +``` + +You can instantiate the file parameter using `FileParameter.Create` that accepts a bytes array, or `FileParameter.FromFile`, which will load the file from disk. + +There are also extension functions that wrap the creation of `FileParameter` inside: + +```csharp +// Adds a file from disk +AddFile(parameterName, filePath, contentType); + +// Adds an array of bytes +AddFile(parameterName, bytes, fileName, contentType); + +// Adds a stream returned by the getFile function +AddFile(parameterName, getFile, fileName, contentType); +``` + +Remember that `AddFile` will set all the necessary headers, so please don't try to set content headers manually. + +You can also provide file upload options to the `AddFile` call. The options are: +- `DisableFilenameEncoding` (default `false`): if set to `true`, RestSharp will not encode the file name in the `Content-Disposition` header +- `DisableFilenameStar` (default `true`): if set to `true`, RestSharp will not add the `filename*` parameter to the `Content-Disposition` header + +Example of using the options: + +```csharp +var options = new FileParameterOptions { + DisableFilenameEncoding = true, + DisableFilenameStar = false +}; +request.AddFile("file", filePath, options: options); +``` + +The options specified in the snippet above usually help when you upload files with non-ASCII characters in their names. + +### Downloading binary data + +There are two functions that allow you to download binary data from the remote API. + +First, there's `DownloadDataAsync`, which returns `Task`. This function allows you to open a stream reader and asynchronously stream large responses to memory or disk. + + +## Reusing HttpClient + +RestSharp uses `HttpClient` internally to make HTTP requests. It's possible to reuse the same `HttpClient` instance for multiple `RestClient` instances. This is useful when you want to share the same connection pool between multiple `RestClient` instances. + +One way of doing it is to use `RestClient` constructors that accept an instance of `HttpClient` or `HttpMessageHandler` as an argument. Note that in that case not all the options provided via `RestClientOptions` will be used. Here is the list of options that will work: + +- `BaseAddress` will be used to set the base address of the `HttpClient` instance if base address is not set there already. +- `MaxTimeout` +- `UserAgent` will be added to the `RestClient.DefaultParameters` list as a HTTP header. This will be added to each request made by the `RestClient`, and the `HttpClient` instance will not be modified. This is to allow the `HttpClient` instance to be reused for scenarios where different `User-Agent` headers are required. +- `Expect100Continue` + +Another option is to use a simple HTTP client factory as described [above](#simple-factory). + +## Blazor support + +Inside a Blazor webassembly app, you can make requests to external API endpoints. Microsoft examples show how to do it with `HttpClient`, and it's also possible to use RestSharp for the same purpose. + +You need to remember that webassembly has some platform-specific limitations. Therefore, you won't be able to instantiate `RestClient` using all of its constructors. In fact, you can only use `RestClient` constructors that accept `HttpClient` or `HttpMessageHandler` as an argument. If you use the default parameterless constructor, it will call the option-based constructor with default options. The options-based constructor will attempt to create an `HttpMessageHandler` instance using the options provided, and it will fail with Blazor, as some of those options throw thw "Unsupported platform" exception. + +Here is an example how to register the `RestClient` instance globally as a singleton: + +```csharp +builder.Services.AddSingleton(new RestClient(new HttpClient())); +``` + +Then, on a page you can inject the instance: + +```html +@page "/fetchdata" +@using RestSharp +@inject RestClient _restClient +``` + +And then use it: + +```csharp +@code { + private WeatherForecast[]? forecasts; + + protected override async Task OnInitializedAsync() { + forecasts = await _restClient.GetJsonAsync("http://localhost:5104/weather"); + } + + public class WeatherForecast { + public DateTime Date { get; set; } + public int TemperatureC { get; set; } + public string? Summary { get; set; } + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + } +} +``` + +In this case, the call will be made to a WebAPI server hosted at `http://localhost:5104/weather`. Remember that if the WebAPI server is not hosting the webassembly itself, it needs to have a CORS policy configured to allow the webassembly origin to access the API endpoint from the browser. diff --git a/docs/versioned_docs/version-v111/advanced/_category_.json b/docs/versioned_docs/version-v111/advanced/_category_.json new file mode 100644 index 000000000..f395bdfe4 --- /dev/null +++ b/docs/versioned_docs/version-v111/advanced/_category_.json @@ -0,0 +1,7 @@ +{ + "label": "Advanced topics", + "position": 4, + "link": { + "type": "generated-index" + } +} diff --git a/docs/versioned_docs/version-v111/advanced/authenticators.md b/docs/versioned_docs/version-v111/advanced/authenticators.md new file mode 100644 index 000000000..14196d97c --- /dev/null +++ b/docs/versioned_docs/version-v111/advanced/authenticators.md @@ -0,0 +1,185 @@ +# Authenticators + +RestSharp includes authenticators for basic HTTP, OAuth1 and token-based (JWT and OAuth2). + +There are two ways to set the authenticator: client-wide or per-request. + +Set the client-wide authenticator by assigning the `Authenticator` property of `RestClientOptions`: + +```csharp +var options = new RestClientOptions("https://example.com") { + Authenticator = new HttpBasicAuthenticator("username", "password") +}; +var client = new RestClient(options); +``` + +To set the authenticator per-request, assign the `Authenticator` property of `RestRequest`: + +```csharp +var request = new RestRequest("/api/users/me") { + Authenticator = new HttpBasicAuthenticator("username", "password") +}; +var response = await client.ExecuteAsync(request, cancellationToken); +``` + +## Basic authentication + +The `HttpBasicAuthenticator` allows you pass a username and password as a basic `Authorization` header using a base64 encoded string. + +```csharp +var options = new RestClientOptions("https://example.com") { + Authenticator = new HttpBasicAuthenticator("username", "password") +}; +var client = new RestClient(options); +``` + +## OAuth1 + +For OAuth1 authentication the `OAuth1Authenticator` class provides static methods to help generate an OAuth authenticator. +OAuth1 authenticator will add the necessary OAuth parameters to the request, including signature. + +The authenticator will use `HMAC SHA1` to create a signature by default. +Each static function to create the authenticator allows you to override the default and use another method to generate the signature. + +### Request token + +Getting a temporary request token is the usual first step in the 3-legged OAuth1 flow. +Use `OAuth1Authenticator.ForRequestToken` function to get the request token authenticator. +This method requires a `consumerKey` and `consumerSecret` to authenticate. + +```csharp +var options = new RestClientOptions("https://api.twitter.com") { + Authenticator = OAuth1Authenticator.ForRequestToken(consumerKey, consumerSecret) +}; +var client = new RestClient(options); +var request = new RestRequest("oauth/request_token"); +``` + +The response should contain the token and the token secret, which can then be used to complete the authorization process. +If you need to provide the callback URL, assign the `CallbackUrl` property of the authenticator to the callback destination. + +### Access token + +Getting an access token is the usual third step in the 3-legged OAuth1 flow. +This method retrieves an access token when provided `consumerKey`, `consumerSecret`, `oauthToken`, and `oauthTokenSecret`. +If you don't have a token for this call, you need to make a call to get the request token as described above. + +```csharp +var authenticator = OAuth1Authenticator.ForAccessToken( + consumerKey, consumerSecret, oauthToken, oauthTokenSecret +); +var options = new RestClientOptions("https://api.twitter.com") { + Authenticator = authenticator +}; +var client = new RestClient(options); +var request = new RestRequest("oauth/access_token"); +``` + +If the second step in 3-leg OAuth1 flow returned a verifier value, you can use another overload of `ForAccessToken`: + +```csharp +var authenticator = OAuth1Authenticator.ForAccessToken( + consumerKey, consumerSecret, oauthToken, oauthTokenSecret, verifier +); +``` + +The response should contain the access token that can be used to make calls to protected resources. + +For refreshing access tokens, use one of the two overloads of `ForAccessToken` that accept `sessionHandle`. + +### Protected resource + +When the access token is available, use `ForProtectedResource` function to get the authenticator for accessing protected resources. + +```csharp +var authenticator = OAuth1Authenticator.ForAccessToken( + consumerKey, consumerSecret, accessToken, accessTokenSecret +); +var options = new RestClientOptions("https://api.twitter.com/1.1") { + Authenticator = authenticator +}; +var client = new RestClient(options); +var request = new RestRequest("statuses/update.json", Method.Post) + .AddParameter("status", "Hello Ladies + Gentlemen, a signed OAuth request!") + .AddParameter("include_entities", "true"); +``` + +### xAuth + +xAuth is a simplified version of OAuth1. It allows sending the username and password as `x_auth_username` and `x_auth_password` request parameters and directly get the access token. xAuth is not widely supported, but RestSharp still allows using it. + +Create an xAuth authenticator using `OAuth1Authenticator.ForClientAuthentication` function: + +```csharp +var authenticator = OAuth1Authenticator.ForClientAuthentication( + consumerKey, consumerSecret, username, password +); +``` + +### 0-legged OAuth + +The access token authenticator can be used in 0-legged OAuth scenarios by providing `null` for the `consumerSecret`. + +```csharp +var authenticator = OAuth1Authenticator.ForAccessToken( + consumerKey, null, oauthToken, oauthTokenSecret +); +``` + +## OAuth2 + +RestSharp has two very simple authenticators to send the access token as part of the request. + +`OAuth2UriQueryParameterAuthenticator` accepts the access token as the only constructor argument, and it will send the provided token as a query parameter `oauth_token`. + +`OAuth2AuthorizationRequestHeaderAuthenticator` has two constructors. One only accepts a single argument, which is the access token. The other constructor also allows you to specify the token type. The authenticator will then add an `Authorization` header using the specified token type or `OAuth` as the default token type, and the token itself. + +For example: + +```csharp +var authenticator = new OAuth2AuthorizationRequestHeaderAuthenticator( + token, "Bearer" +); +var options = new RestClientOptions("https://example.com") { + Authenticator = authenticator +}; +var client = new RestClient(options); +``` + +The code above will tell RestSharp to send the bearer token with each request as a header. Essentially, the code above does the same as the sample for `JwtAuthenticator` below. + +As those authenticators don't do much to get the token itself, you might be interested in looking at our [sample OAuth2 authenticator](../usage/example.md#authenticator), which requests the token on its own. + +## JWT + +The JWT authentication can be supported by using `JwtAuthenticator`. It is a very simple class that can be constructed like this: + +```csharp +var authenticator = new JwtAuthenticator(myToken); +var options = new RestClientOptions("https://example.com") { + Authenticator = authenticator +}; +var client = new RestClient(options); +``` + +For each request, it will add an `Authorization` header with the value `Bearer `. + +As you might need to refresh the token from, you can use the `SetBearerToken` method to update the token. + +## Custom authenticator + +You can write your own implementation by implementing `IAuthenticator` and +registering it with your RestClient: + +```csharp +var authenticator = new SuperAuthenticator(); // implements IAuthenticator +var options = new RestClientOptions("https://example.com") { + Authenticator = authenticator +}; +var client = new RestClient(options); +``` + +The `Authenticate` method is the very first thing called upon calling `RestClient.Execute` or `RestClient.Execute`. +It gets the `RestRequest` currently being executed giving you access to every part of the request data (headers, parameters, etc.) + +You can find an example of a custom authenticator that fetches and uses an OAuth2 bearer token [here](../usage/example.md#authenticator). diff --git a/docs/versioned_docs/version-v111/advanced/configuration.md b/docs/versioned_docs/version-v111/advanced/configuration.md new file mode 100644 index 000000000..6fbfaed33 --- /dev/null +++ b/docs/versioned_docs/version-v111/advanced/configuration.md @@ -0,0 +1,230 @@ +--- +title: Configuration +description: Learn how to configure RestClient for non-trivial use cases. +sidebar_position: 1 +--- + +# Configuring RestClient + +This page describes how to create and configure `RestClient`. + +## Basic configuration + +The primary `RestClient` constructor accepts an instance of `RestClientOptions`. Most of the time, default option values don't need to be changed. However, in some cases, you'd want to configure the client differently, so you'd need to change some of the options in your code. The constructor also contains a few optional parameters for additional configuration that is not covered by client options. Here's the constructor signature: + +```csharp +public RestClient( + RestClientOptions options, + ConfigureHeaders? configureDefaultHeaders = null, + ConfigureSerialization? configureSerialization = null, + bool useClientFactory = false +) +``` + +Constructor parameters are: + +| Name | Description | Mandatory | +|-------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------| +| options | Client options | Yes | +| configureDefaultHeaders | Function to configure headers. Allows to configure default headers for `HttpClient`. Most of the time you'd prefer using `client.AddDefaultHeader` instead. | No | +| configureSerialization | Function to configure client serializers with non-default options or to use a different serializer ([learn more](serialization.md)) | No | +| useClientFactory | Instructs the client to use `SimpleFactory` ([learn more](../usage/client#simple-factory)) to get an `HttpClient` instance | No | + +Here's an example of how to create a client using client options: + +```csharp +var options = new RestClientOptions("https://localhost:5000/api") { + DisableCharset = true +}; +var client = new RestClient(options); +``` + +When you only need to set the base URL, you can use a simplified constructor: + +```csharp +var client = new RestClient("https://localhost:5000/api"); +``` + +The simplified constructor will create an instance of client options and set the base URL provided as the constructor argument. + +Finally, you can override properties of default options using a configuration function. Here's the constructor signature that supports this method: + +```csharp +public RestClient( + ConfigureRestClient? configureRestClient = null, + ConfigureHeaders? configureDefaultHeaders = null, + ConfigureSerialization? configureSerialization = null, + bool useClientFactory = false +) +``` + +For example: + +```csharp +var client = new RestClient(options => { + options.BaseUrl = new Url("https://localhost:5000/api"), + options.DisableCharset = true +}); +``` + +You can also provide the base URL as a constructor argument like this: + +```csharp +var client = new RestClient("https://localhost:5000/api", options => { + options.DisableCharset = true +}); +``` + +## Using custom HttpClient + +By default, RestSharp creates an instance of `HttpClient` configured using the client options, and keeps it during the lifetime of the client. When the `RestClient` instance gets disposed, it also disposes the `HttpClient` instance. + +There might be a case when you need to provide your own `HttpClient`. For example, you would want to use `HttpClient` created by HTTP client factory. RestSharp allows you to do it by using additional constructors. These constructors are: + +```csharp +// Create a client using an existing HttpClient and RestClientOptions (optional) +public RestClient( + HttpClient httpClient, + RestClientOptions? options, + bool disposeHttpClient = false, + ConfigureSerialization? configureSerialization = null +) + +// Create a client using an existing HttpClient and optional RestClient configuration function +public RestClient( + HttpClient httpClient, + bool disposeHttpClient = false, + ConfigureRestClient? configureRestClient = null, + ConfigureSerialization? configureSerialization = null +) +``` + +The `disposeHttpClient` argument tells the client to dispose `HttpClient` when the client itself gets disposed. It's set to `false` by default as when the `HttpClient` is provided from the outside, it should normally be disposed on the outside as well. + +## Using custom message handler + +Unless you use an external instance of `HttpClient`, the `RestClient` creates one when being constructed, and it will use the default HTTP message handler, configured using `RestClientOptions`. Normally, you'd get a `SocketHttpHandler` with modern .NET, and `WinHttpHandler` with .NET Framework. + +There might be a case when you need to configure the HTTP message handler. For example, you want to add a delegating message handler. RestSharp allows you to do it by using additional constructors. There's one constructor that allows you to pass the custom `HttpMessageHandler`: + +```csharp +public RestClient( + HttpMessageHandler handler, + bool disposeHandler = true, + ConfigureRestClient? configureRestClient = null, + ConfigureSerialization? configureSerialization = null +) +``` + +This constructor will create a new `HttpClient` instance using the provided message handler. As RestSharp will dispose the `HttpClient` instance when the `RestClient` instance gets disposed, the handler will be disposed as well. If you want to change that and keep the handler, set the `disposeHandler` parameter to `false`. + +:::note +When using a custom message handler, RestSharp **will not** configure it with client options, which are normally used to configure the handler created by RestSharp. +::: + +Another way to customize the message handler is to allow RestSharp to create a handler, but then configure it, or wrap it in a delegating handler. It can be done by using the `RestClientOptions.ConfigureMessageHandler` property. It can be set to a function that receives the handler created by RestSharp and returned either the same handler with different settings, or a new handler. + +For example, if you want to use `MockHttp` and its handler for testing, you can do it like this: + +```csharp +var mockHttp = new MockHttpMessageHandler(); +// Configure the MockHttp handler to do the checks +... + +var options = new RestClientOptions(Url) { + ConfigureMessageHandler = _ => mockHttp +}; +using var client = new RestClient(options); +``` + +In this example, we are reassigning the handler to MockHttp, so the handler created by RestSharp isn't used. In other cases you want to use delegating handlers as middleware, so you'd pass the handler created by RestSharp to the delegating handler: + +```csharp +var options = new RestClientOptions(Url) { + ConfigureMessageHandler = handler => new MyDelegatingHandler(handler) +}; +using var client = new RestClient(options); +``` + +## Client options + +RestSharp allows configuring `RestClient` using client options, as mentioned at the beginning of this page. Below, you find more details about available options. + +| Option | Description | +|----------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `BaseUrl` | Client base URL. It can also be provided as the `RestClientOptions` constructor argument. | +| `ConfigureMessageHandler` | Configures the HTTP message handler (see above). | +| `CalculateResponseStatus` | Function to calculate a different response status from `HttpResponseMessage`. By default, the request is considered as complete if it returns a successful status code or 404. | +| `Authenticator` | Client-level authenticator. Read more about authenticators [here](authenticators.md). | +| `Interceptors` | A collector of interceptors. Read more about interceptors [here](interceptors.md). | +| `Credentials` | Instance of `ICredentials` used for NTLM or Kerberos authentication. Not supported in browsers. | +| `UseDefaultCredentials` | Whether to use default OS credentials for NTLM or Kerberos authentication. Not supported in browsers. | +| `DisableCharset` | When set to `true`, the `Content-Type` header won't have the `charset` portion. Some older web servers don't understand the `charset` portion in the header and fail to process the request. | +| `AutomaticDecompression` | Allows customizing supported decompression methods. Default is `All` except for .NET Framework that only support `GZip`. Not supported in browsers. | +| `MaxRedirects` | The number of redirects to follow. Not supported in browsers. | +| `ClientCertificates` | A collection of X.509 client certificates to be used for authentication. Not supported in browsers. | +| `Proxy` | Can be used if the client needs to use an explicit, non-default proxy. Not supported in browsers, on iOS and tvOS. | +| `CachePolicy` | Shortcut for setting the default value for `Cache-Control` header. | +| `FollowRedirects` | Instructs the client to follow redirects. Default is `true`. | +| `Expect100Continue` | Gets or sets a value that indicates if the `Expect` header for an HTTP request contains `Continue`. | +| `UserAgent` | Allows overriding the default value for `User-Agent` header, which is `RestSharp/{version}`. | +| `PreAuthenticate` | Gets or sets a value that indicates whether the client sends an `Authorization` header with the request. Not supported in browsers. | +| `RemoteCertificateValidationCallback` | Custom function to validate the server certificate. Normally, it's used when the server uses a certificate that isn't trusted by default. | +| `BaseHost` | Value for the `Host` header sent with each request. | +| `CookieContainer` | Custom cookie container that will be shared among all calls made by the client. Normally not required as RestSharp handles cookies without using a client-level cookie container. | +| `MaxTimeout` | Client-level timeout in milliseconds. If the request timeout is also set, this value isn't used. | +| `Encoding` | Default request encoding. Override it only if you don't use UTF-8. | +| `ThrowOnDeserializationError` | Forces the client to throw if it fails to deserialize the response. Remember that not all deserialization issues forces the serializer to throw. Default is `false`, so the client will return a `RestResponse` with deserialization exception details. Only relevant for `Execute...` functions. | +| `FailOnDeserializationError` | When set to `true`, if the client fails to deserialize the response, the response object will have status `Failed`, although the HTTP calls might have been successful. Default is `true`. | +| `ThrowOnAnyError` | When set to `true`, the client will re-throw any exception from `HttpClient`. Default is `false`. Only applies for `Execute...` functions. | +| `AllowMultipleDefaultParametersWithSameName` | By default, adding parameters with the same name is not allowed. You can override this behaviour by setting this property to `true`. | +| `Encode` | A function to encode URLs, the default is a custom RestSharp function based on `Uri.EscapeDataString()`. Set it if you need a different way to do the encoding. | +| `EncodeQuery` | A function to encode URL query parameters. The default is the same function as for `Encode` property. | + +Some of the options are used by RestSharp code, but some are only used to configure the `HttpMessageHandler`. These options are: +- `Credentials` +- `UseDefaultCredentials` +- `AutomaticDecompression` +- `PreAuthenticate` +- `MaxRedirects` +- `RemoteCertificateValidationCallback` +- `ClientCertificates` +- `FollowRedirects` +- `Proxy` + +:::note +If setting these options to non-default values produce no desirable effect, check if your framework and platform supports them. RestSharp doesn't change behaviour based on values of those options. +::: + +The `IRestClient` interface exposes the `Options` property, so any option can be inspected at runtime. However, RestSharp converts the options object provided to the client constructor to an immutable object. Therefore, no client option can be changed after the client is instantiated. It's because changing client options at runtime can produce issues in concurrent environments, effectively rendering the client as not thread-safe. Apart from that, changing the options that are used to create the message handler would require re-creating the handler, and also `HttpClient`, which should not be done at runtime. + +## Configuring requests + +Client options apply to all requests made by the client. Sometimes, you want to fine-tune particular requests, so they execute with custom configuration. It's possible to do using properties of `RestRequest`, described below. + +| Name | Description | +|------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `AlwaysMultipartFormData` | When set to `true`, the request will be sent as a multipart form, even though it's not required. By default, RestSharp only sends requests with multiple attachments as multipart forms. Default is `false`. | +| `AlwaysSingleFileAsContent` | When set to true, the request with file attachment will not be sent as a multipart form, but as plain content. Default is `false`. It cannot be set to `true` when `AlwaysMultipartFormData` is set to `true`, or when the request has `POST` parameters. | +| `MultipartFormQuoteBoundary` | Default is `true`, which means that the form boundary string will be wrapped in quotes. If the server has an issue with that, setting this to `false` will remove quotes around the boundary. | +| `FormBoundary` | Allows specifying a custom multipart form boundary instead of using the default random string. | +| `RequestParameters` | Collection of request parameters. Normally, you won't need to use it as parameters are added to the request using `Add...` functions. | +| `CookieContainer` | Custom request-level cookie container. Default is `null`. You can still set request cookies using `AddCookie` and get response cookies from the response object without using cooking container. | +| `Authenticator` | Overrides the client-level authenticator. | +| `Files` | Collection of file parameters, read-only. Use `AddFile` for adding files to the request. | +| `Method` | Request HTTP method, default is `GET`. Only needed when using `Execute` or `ExecuteAsync` as other functions like `ExecutePostAsync` will override the request method. | +| `TImeout` | Overrides the client-level timeout. | +| `Resource` | Resource part of the remote endpoint URL. For example, when using the client-level base URL `https://localhost:5000/api` and `Resource` set to `weather`, the request will be sent to `https://localhost:5000/api/weather`. It can container resource placeholders to be used in combination with `AddUrlSegment` | +| `RequestFormat` | Identifies the request as JSON, XML, binary, or none. Rarely used because the client will set the request format based on the body type if functions like `AddJsonBody` or `AddXmlBody` are used. | +| `RootElement` | Used by the default deserializers to determine where to start deserializing from. Only supported for XML responses. Does not apply to requests. | +| `OnBeforeDeserialization` | **Obsolete** A function to be called before the response is deserializer. Allows changing the content before calling the deserializer. Use [interceptors](interceptors.md) instead. | +| `OnBeforeRequest` | **Obsolete** A function to be called right before the request is executed by `HttpClient`. It receives an instance of `HttpRequestMessage`. Use [interceptors](interceptors.md) instead. | +| `OnAfterRequest` | **Obsolete** A function to be called right after the request is executed by `HttpClient`. It receives an instance of `HttpResponseMessage`. Use [interceptors](interceptors.md) instead. | +| `Attempts` | When the request is being resent to retry, the property value increases by one. | +| `CompletionOption` | Instructs the client on when it should consider the request to be completed. The default is `ResponseContentRead`. It is automatically changed to `ResponseHeadersRead` when using async download functions or streaming. | +| `CachePolicy` | Overrides the client cache policy. | +| `ResponseWriter` | Allows custom handling of the response stream. The function gets the raw response stream and returns another stream or `null`. Cannot be used in combination with `AdvancedResponseWriter`. | +| `AdvancedResponseWriter` | Allows custom handling of the response. The function gets an instance of `HttpResponseMessage` and an instance of `RestRequest`. It must return an instance of `RestResponse`, so it effectively overrides RestSharp default functionality for creating responses. | +| `Interceptors` | Allows adding interceptors to the request. Both client-level and request-level interceptors will be called. | + +The table below contains all configuration properties of `RestRequest`. To learn more about adding request parameters, check the [usage page](../usage/request.md) section about creating requests with parameters. diff --git a/docs/versioned_docs/version-v111/advanced/error-handling.md b/docs/versioned_docs/version-v111/advanced/error-handling.md new file mode 100644 index 000000000..7b3be11fc --- /dev/null +++ b/docs/versioned_docs/version-v111/advanced/error-handling.md @@ -0,0 +1,67 @@ +# Error handling + +If there is a network transport error (network is down, failed DNS lookup, etc), or any kind of server error (except 404), `RestResponse.ResponseStatus` will be set to `ResponseStatus.Error`, otherwise it will be `ResponseStatus.Completed`. + +If an API returns a 404, `ResponseStatus` will still be `Completed`. If you need access to the HTTP status code returned you will find it at `RestResponse.StatusCode`. +The `Status` property is an indicator of completion independent of the API error handling. + +Normally, RestSharp doesn't throw an exception if the request fails. + +However, it is possible to configure RestSharp to throw in different situations, when it normally doesn't throw +in favour of giving you the error as a property. + +| Property | Behavior | +|-------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `FailOnDeserializationError` | Changes the default behavior when failed deserialization results in a successful response with an empty `Data` property of the response. Setting this property to `true` will tell RestSharp to consider failed deserialization as an error and set the `ResponseStatus` to `Error` accordingly. | +| `ThrowOnDeserializationError` | Changes the default behavior when failed deserialization results in empty `Data` property of the response. Setting this property to `true` will tell RestSharp to throw when deserialization fails. | +| `ThrowOnAnyError` | Setting this property to `true` changes the default behavior and forces RestSharp to throw if any errors occurs when making a request or during deserialization. | + +Those properties are available for the `RestClientOptions` and will be used for all request made with the client instance. + +For example, you can configure the client to throw an exception if any error occurs when making a request, or when a request returns a non-successful HTTP status code: + +```csharp +var options = new RestClientOptions(url) { + ThrowOnAnyError = true +}; +var client = new RestClient(options); +var request = new RestRequest("resource/{id}").AddUrlSegment("id", 123); +// 👇 will throw if the request fails +var response = await client.ExecuteGetAsync(request); +``` + +:::warning +Please be aware that deserialization failures will only work if the serializer throws an exception when deserializing the response. +Many serializers don't throw by default, and just return a `null` result. RestSharp is unable to figure out why `null` is returned, so it won't fail in this case. +Check the serializer documentation to find out if it can be configured to throw on deserialization error. +::: + +There are also slight differences on how different overloads handle exceptions. + +Asynchronous generic methods `GetAsync`, `PostAsync` and so on, which aren't a part of `RestClient` interface (those methods are extension methods) return `Task`. It means that there's no `RestResponse` to set the response status to error. We decided to throw an exception when such a request fails. It is a trade-off between the API consistency and usability of the library. Usually, you only need the content of `RestResponse` instance to diagnose issues and most of the time the exception would tell you what's wrong. + +Below, you can find how different extensions deal with errors. Note that functions, which don't throw by default, will throw exceptions when `ThrowOnAnyError` is set to `true`. + +| Function | Throws on errors | +|:----------------------|:-----------------| +| `ExecuteAsync` | No | +| `ExecuteGetAsync` | No | +| `ExecuteGetAsync` | No | +| `ExecutePostAsync` | No | +| `ExecutePostAsync` | No | +| `ExecutePutAsync` | No | +| `ExecutePutAsync` | No | +| `GetAsync` | Yes | +| `GetAsync` | Yes | +| `PostAsync` | Yes | +| `PostAsync` | Yes | +| `PatchAsync` | Yes | +| `PatchAsync` | Yes | +| `DeleteAsync` | Yes | +| `DeleteAsync` | Yes | +| `OptionsAsync` | Yes | +| `OptionsAsync` | Yes | +| `HeadAsync` | Yes | +| `HeadAsync` | Yes | + +In addition, all the functions for JSON requests, like `GetJsonAsync` and `PostJsonAsync` throw an exception if the HTTP call fails. diff --git a/docs/versioned_docs/version-v111/advanced/interceptors.md b/docs/versioned_docs/version-v111/advanced/interceptors.md new file mode 100644 index 000000000..a3bf7a9bc --- /dev/null +++ b/docs/versioned_docs/version-v111/advanced/interceptors.md @@ -0,0 +1,84 @@ +--- +title: Interceptors +--- + +## Intercepting requests and responses + +Interceptors are a powerful feature of RestSharp that allows you to modify requests and responses before they are sent or received. You can use interceptors to add headers, modify the request body, or even cancel the request. You can also use interceptors to modify the response before it is returned to the caller. + +### Implementing an interceptor + +To implement an interceptor, you need to create a class that inherits the `Interceptor` base class. The base class implements all interceptor methods as virtual, so you can override them in your derived class. + +Methods that you can override are: +- `BeforeRequest(RestRequest request, CancellationToken cancellationToken)` +- `AfterRequest(RestResponse response, CancellationToken cancellationToken)` +- `BeforeHttpRequest(HttpRequestMessage requestMessage, CancellationToken cancellationToken)` +- `AfterHttpRequest(HttpResponseMessage responseMessage, CancellationToken cancellationToken)` +- `BeforeDeserialization(RestResponse response, CancellationToken cancellationToken)` + +All those functions must return a `ValueTask` instance. + +Here's an example of an interceptor that adds a header to a request: + +```csharp +// This interceptor adds a header to the request +// You'd not normally use this interceptor, as RestSharp already has a method +// to add headers to the request +class HeaderInterceptor(string headerName, string headerValue) : Interceptors.Interceptor { + public override ValueTask BeforeHttpRequest(HttpRequestMessage requestMessage, CancellationToken cancellationToken) { + requestMessage.Headers.Add(headerName, headerValue); + return ValueTask.CompletedTask; + } +} +``` + +Because interceptor functions return `ValueTask`, you can use `async` and `await` inside them. + +### Using an interceptor + +It's possible to add as many interceptors as you want, both to the client and to the request. The interceptors are executed in the order they were added. + +Adding interceptors to the client is done via the client options: + +```csharp +var options = new RestClientOptions("https://api.example.com") { + Interceptors = [new HeaderInterceptor("Authorization", token)] +}; +var client = new RestClient(options); +``` + +When you add an interceptor to the client, it will be executed for every request made by that client. + +You can also add an interceptor to a specific request: + +```csharp +var request = new RestRequest("resource") { + Interceptors = [new HeaderInterceptor("Authorization", token)] +}; +``` + +In this case, the interceptor will only be executed for that specific request. + +### Deprecation notice + +Interceptors aim to replace the existing request hooks available in RestSharp prior to version 111.0. Those hooks are marked with `Obsolete` attribute and will be removed in the future. If you are using those hooks, we recommend migrating to interceptors as soon as possible. + +To make the migration easier, RestSharp provides a class called `CompatibilityInterceptor`. It has properties for the hooks available in RestSharp 110.0 and earlier. You can use it to migrate your code to interceptors without changing the existing logic. + +For example, a code that uses `OnBeforeRequest` hook: + +```csharp +var request = new RestRequest("success"); +request.OnBeforeDeserialization += _ => throw new Exception(exceptionMessage); +``` + +Can be migrated to interceptors like this: + +```csharp +var request = new RestRequest("success") { + Interceptors = [new CompatibilityInterceptor { + OnBeforeDeserialization = _ => throw new Exception(exceptionMessage) + }] +}; +``` \ No newline at end of file diff --git a/docs/versioned_docs/version-v111/advanced/serialization.md b/docs/versioned_docs/version-v111/advanced/serialization.md new file mode 100644 index 000000000..b7092519a --- /dev/null +++ b/docs/versioned_docs/version-v111/advanced/serialization.md @@ -0,0 +1,148 @@ +# Serialization + +One of the most common reasons to choose RestSharp over plain `HttpClient` is its rich build-in serialization support. RestSharp allows adding complex objects as request body to be serialized when making a call to an API endpoint, and deserializing the response to a given .NET type. RestSharp supports JSON and XML serialization and deserialization by default. In addition, you can use a CSV serializer or write your own. + +In contrast to `System.Net.Http.Json` package that contains `HttpClient` extensions to make `GET` or `POST` calls using JSON, RestSharp support JSON responses for all HTTP methods, not just for `GET`. + +## Configuration + +:::tip +The default behavior of RestSharp is to swallow deserialization errors and return `null` in the `Data` +property of the response. Read more about it in the [Error Handling](error-handling.md). +::: + +You can tell RestSharp to use a custom serializer by using the `configureSerialization` constructor parameter: + +```csharp +var client = new RestClient( + options, + configureSerialization: s => s.UseSerializer(() => new CustomSerializer()); +); +``` + +All RestSharp serializers implement the `IRestSerializer` interface. Among other things, the interface requires implementing the `AcceptedContentTypes` property, which must return a collection of content types supported by the serializer. Being configured to use certain serializers, RestSharp populates the `Accept` header accordingly, so it doesn't need to be set manually. + +When making a call, RestSharp sets the request content type according to the request body type. For example, when you use `AddJsonBody`, the content type is set to `application/json`. Normally, you won't need to set the `Content-Type` header manually. If you need to set a custom content type for a JSON call, you can use the optional `contentType` argument of `AddJsonBody`, for example: + +```csharp +request.AddJsonBody(data, "text/json"); +``` + +## JSON + +The default JSON serializer uses `System.Text.Json`, which is a part of .NET since .NET 6. For earlier versions, it is added as a dependency. There are also a few serializers provided as additional packages. + +By default, RestSharp will use `JsonSerializerDefaults.Web` configuration. If necessary, you can specify your own options: + +```csharp +var client = new RestClient( + options, + configureSerialization: s => s.UseSystemTextJson(new JsonSerializerOptions {...}) +); +``` + +## XML + +The default XML serializer is `DotNetXmlSerializer`, which uses `System.Xml.Serialization` library from .NET. + +In previous versions of RestSharp, the default XML serializer was a custom RestSharp XML serializer. To make the code library size smaller, that serializer is now available as a separate package [`RestSharp.Serializers.Xml`](https://www.nuget.org/packages/RestSharp.Serializers.Xml). +You can add it back if necessary by installing the package and adding it to the client: + +```csharp +var client = new RestClient( + options, + configureSerialization: s => s.UseXmlSerializer() +); +``` + +As before, you can supply three optional arguments for a custom namespace, custom root element, and if you want to use `SerializeAs` and `DeserializeAs` attributed. + +## NewtonsoftJson (aka Json.Net) + +The `NewtonsoftJson` package is the most popular JSON serializer for .NET. It handles all possible scenarios and is very configurable. Such a flexibility comes with the cost of performance. If you need speed, keep the default JSON serializer. + +RestSharp support Json.Net serializer via a separate package [`RestSharp.Serializers.NewtonsoftJson`](https://www.nuget.org/packages/RestSharp.Serializers.NewtonsoftJson). + +:::warning +Please note that `RestSharp.Newtonsoft.Json` package is not provided by RestSharp, is marked as obsolete on NuGet, and no longer supported by its creator. +::: + +Use the extension method provided by the package to configure the client: + +```csharp +var client = new RestClient( + options, + configureSerialization: s => s.UseNewtonsoftJson() +); +``` + +The serializer configures some options by default: + +```csharp +JsonSerializerSettings DefaultSettings = new JsonSerializerSettings { + ContractResolver = new CamelCasePropertyNamesContractResolver(), + DefaultValueHandling = DefaultValueHandling.Include, + TypeNameHandling = TypeNameHandling.None, + NullValueHandling = NullValueHandling.Ignore, + Formatting = Formatting.None, + ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor +}; +``` + +If you need to use different settings, you can supply your instance of +`JsonSerializerSettings` as a parameter for the extension method. + +## CSV + +A separate package `RestSharp.Serializers.CsvHelper` provides a CSV serializer for RestSharp. It is based on the +`CsvHelper` library. + +Use the extension method provided by the package to configure the client: + +```csharp +var client = new RestClient( + options, + configureSerialization: s => s.UseCsvHelper() +); +``` + +You can also supply your instance of `CsvConfiguration` as a parameter for the extension method. + +```csharp +var client = new RestClient( + options, + configureSerialization: s => s.UseCsvHelper( + new CsvConfiguration(CultureInfo.InvariantCulture) {...} + ) +); +``` + +## Custom + +You can also implement your custom serializer. To support both serialization and +deserialization, you must implement the `IRestSerializer` interface. + +Here is an example of a custom serializer that uses `System.Text.Json`: + +```csharp +public class SimpleJsonSerializer : IRestSerializer { + public string? Serialize(object? obj) => obj == null ? null : JsonSerializer.Serialize(obj); + + public string? Serialize(Parameter bodyParameter) => Serialize(bodyParameter.Value); + + public T? Deserialize(RestResponse response) => JsonSerializer.Deserialize(response.Content!); + + public ContentType ContentType { get; set; } = ContentType.Json; + + public ISerializer Serializer => this; + public IDeserializer Deserializer => this; + public DataFormat DataFormat => DataFormat.Json; + public string[] AcceptedContentTypes => ContentType.JsonAccept; + public SupportsContentType SupportsContentType + => contentType => contentType.Value.EndsWith("json", StringComparison.InvariantCultureIgnoreCase); +} +``` + +The `SupportedContentTypes` function will be used to check if the serializer is able to deserialize the response based on the `Content-Type` response header. + +The `ContentType` property will be used when making a request so the server knows how to handle the payload. diff --git a/docs/versioned_docs/version-v111/changelog.md b/docs/versioned_docs/version-v111/changelog.md new file mode 100644 index 000000000..3fc2ca63f --- /dev/null +++ b/docs/versioned_docs/version-v111/changelog.md @@ -0,0 +1,49 @@ +--- +title: What's new +description: List of changes for the current major version +sidebar_position: 2 +--- + +# Changelog + +This changelog is only maintained since v111. For release notes of previous versions, please check the [Releases page](https://github.com/restsharp/RestSharp/releases) in RestSharp GitHub repository. + +Only the most important or breaking changes are listed there. All other changes can be found in each release on GitHub. + +## v111.3 + +New extensions: +* `RestResponse.GetHeader` for getting one response header value +* `RestResponse.GetHeaders` for getting a collection of header values +* `IRestClient.(Execute)Get(Async)` with string resource instead of a request object +* `IRestClient.(Execute)Delete(Async)` with string resource instead of a request object + +[Full changelog](https://github.com/restsharp/RestSharp/releases/tag/111.3.0) + +## v111.2 + +* `Execute` extensions that were accidentally removed from v111 are back +* Several authenticators got renamed by unintentional refactoring, that change has also been reverted. + +[Full changelog](https://github.com/restsharp/RestSharp/releases/tag/111.2.0) + +## v111.0 + +:::warning +Package for v111.0 and v111.1 are listed as unsupported on NuGet as there are API changes that weren't planned. +Use the patched version v111.2 or later. +::: + +### Breaking changes +* Client option `MaxTimeout` renamed to `Timeout` and changed type to `Timespan` for clarity. It doesn't configure the `HttpClient` timeout anymore. Instead, the same method is used for client and request level timeouts with cancellation tokens. +* Request option `Timeout` changed type to `Timespan` for clarity. + +### New features +* Added [interceptors](advanced/interceptors.md). +* As interceptors provide a better way to interject the request and response execution flow, request properties `OnBeforeRequest`, `OnBeforeDeserialization` and `OnAfterRequest` are marked obsolete and will be removed in future versions. +* Added .NET 8 target. +* Support for uploading files as content without multipart form. +* Added `CacheControl` options to client and requests. +* Allow using `AddJsonBody` to serialize top-level strings. + +[Full changelog](https://github.com/restsharp/RestSharp/releases/tag/111.0.0) \ No newline at end of file diff --git a/docs/versioned_docs/version-v111/intro.md b/docs/versioned_docs/version-v111/intro.md new file mode 100644 index 000000000..8144a61cd --- /dev/null +++ b/docs/versioned_docs/version-v111/intro.md @@ -0,0 +1,112 @@ +--- +sidebar_position: 1 +title: Quick start +--- + +## Introduction + +:::warning +RestSharp v107+ changes the library API surface and its behaviour significantly. We advise looking at [migration](/migration) docs to understand how to migrate to the latest version of RestSharp. +::: + +The main purpose of RestSharp is to make synchronous and asynchronous calls to remote resources over HTTP. As the name suggests, the main audience of RestSharp are developers who use REST APIs. However, RestSharp can call any API over HTTP, as long as you have the resource URI and request parameters that you want to send comply with W3C HTTP standards. + +One of the main challenges of using HTTP APIs for .NET developers is to work with requests and responses of different kinds and translate them to complex C# types. RestSharp can take care of serializing the request body to JSON or XML and deserialize the response. It can also form a valid request URI based on different parameter kinds: path, query, form or body. + +## Getting Started + +Before you can use RestSharp in your application, you need to add the NuGet package. You can do it using your IDE or the command line: + +``` +dotnet add package RestSharp +``` + +### Basic Usage + +If you only have a small number of one-off API requests to perform, you can use RestSharp like this: + +```csharp +using RestSharp; +using RestSharp.Authenticators; + +var options = new RestClientOptions("https://api.twitter.com/1.1") { + Authenticator = new HttpBasicAuthenticator("username", "password") +}; +var client = new RestClient(options); +var request = new RestRequest("statuses/home_timeline.json"); +// The cancellation token comes from the caller. You can still make a call without it. +var response = await client.GetAsync(request, cancellationToken); +``` + +It will return a `RestResponse` back, which contains all the information returned from the remote server. +You have access to the headers, content, HTTP status and more. + +You can also use generic overloads like `Get` to automatically deserialize the response into a .NET class. + +For example: + +```csharp +using RestSharp; +using RestSharp.Authenticators; + +var options = new RestClientOptions("https://api.twitter.com/1.1") { + Authenticator = new HttpBasicAuthenticator("username", "password") +}; +var client = new RestClient(options); + +var request = new RestRequest("statuses/home_timeline.json"); + +// The cancellation token comes from the caller. You can still make a call without it. +var timeline = await client.GetAsync(request, cancellationToken); +``` + +Both snippets above use the `GetAsync` extension, which is a wrapper about `ExecuteGetAsync`, which, in turn, is a wrapper around `ExecuteAsync`. +All `ExecuteAsync` overloads and return the `RestResponse` or `RestResponse`. + +The most important difference is that async methods named after HTTP methods (like `GetAsync` or `PostAsync`) return `Task` instead of `Task>`. It means that you won't get an error response if the request fails as those methods throw an exception for unsuccessful HTTP calls. For keeping the API consistent, non-generic functions like `GetAsync` or `PostAsync` also throw an exception if the request fails, although they return the `Task`. + +Read [here](advanced/error-handling.md) about how RestSharp handles exceptions. + +RestSharp also offers simple ways to call APIs that accept and return JSON payloads. You can use the `GetJsonAsync` and `PostJsonAsync` extension methods, which will automatically serialize the request body to JSON and deserialize the response to the specified type. + +```csharp +var client = new RestClient(options); +var timeline = await client.GetJsonAsync("statuses/home_timeline.json", cancellationToken); +``` + +Read [here](usage/execute#json-requests) about making JSON calls without preparing a request object. + +### Content type + +RestSharp supports sending XML or JSON body as part of the request. To add a body to the request, simply call `AddJsonBody` or `AddXmlBody` method of the `RestRequest` object. + +There is no need to set the `Content-Type` or add the `DataFormat` parameter to the request when using those methods, RestSharp will do it for you. + +RestSharp will also handle both XML and JSON responses and perform all necessary deserialization tasks, depending on the server response type. Therefore, you only need to add the `Accept` header if you want to deserialize the response manually. + +For example, you'd only need these lines to make a request with JSON body: + +```csharp +var request = new RestRequest("address/update").AddJsonBody(updatedAddress); +var response = await client.PostAsync(request); +``` + +It's also possible to make the same call using `PostAsync` shorter syntax: + +```csharp +var response = await PostJsonAsync( + "address/update", request, cancellationToken +); +``` + +Read more about serialization and deserialization [here](advanced/serialization.md). + +### Response + +When you use `ExecuteAsync`, you get an instance of `RestResponse` back. The response object has the `Content` property, which contains the response as string. You can find other useful properties there, like `StatusCode`, `ContentType` and so on. If the request wasn't successful, you'd get a response back with `IsSuccessful` property set to `false` and the error explained in the `ErrorException` and `ErrorMessage` properties. + +When using typed `ExecuteAsync`, you get an instance of `RestResponse` back, which is identical to `RestResponse` but also contains the `T Data` property with the deserialized response. + +None of `ExecuteAsync` overloads throw if the remote server returns an error. You can inspect the response and find the status code, error message, and, potentially, an exception. + +Extensions like `GetAsync` will not return the whole `RestResponse` but just a deserialized response. These extensions will throw an exception if the remote server returns an error. The exception details contain the status code returned by the server. diff --git a/docs/versioned_docs/version-v111/usage/_category_.json b/docs/versioned_docs/version-v111/usage/_category_.json new file mode 100644 index 000000000..bd2045cd6 --- /dev/null +++ b/docs/versioned_docs/version-v111/usage/_category_.json @@ -0,0 +1,7 @@ +{ + "label": "Using RestSharp", + "position": 3, + "link": { + "type": "generated-index" + } +} diff --git a/docs/versioned_docs/version-v111/usage/basics.md b/docs/versioned_docs/version-v111/usage/basics.md new file mode 100644 index 000000000..6bbb5594a --- /dev/null +++ b/docs/versioned_docs/version-v111/usage/basics.md @@ -0,0 +1,23 @@ +--- +sidebar_position: 2 +--- + +# RestSharp basics + +This page describes some of the essential properties and features of RestSharp. + +## What RestSharp does + +Essentially, RestSharp is a wrapper around `HttpClient` that allows you to do the following: +- Add default parameters of any kind (not just headers) to the client, once +- Add parameters of any kind to each request (query, URL segment, form, attachment, serialized body, header) in a straightforward way +- Serialize the payload to JSON or XML if necessary +- Set the correct content headers (content type, disposition, length, etc.) +- Handle the remote endpoint response +- Deserialize the response from JSON or XML if necessary + +## API client + +The best way to call an external HTTP API is to create a typed client, which encapsulates RestSharp calls and doesn't expose the `RestClient` instance in public. + +You can find an example of a Twitter API client on the [Example](example.md) page. diff --git a/docs/versioned_docs/version-v111/usage/client.md b/docs/versioned_docs/version-v111/usage/client.md new file mode 100644 index 000000000..0207c3e25 --- /dev/null +++ b/docs/versioned_docs/version-v111/usage/client.md @@ -0,0 +1,114 @@ +--- +sidebar_position: 3 +title: Creating the client +--- + +## Constructors + +A RestSharp client can be instantiated by one of its constructors. Two most commonly used constructors are: + +#### Only specify the base URL + +You can create an instance of `RestClient` with only a single parameter: the base URL. Even that isn't required as base URL can be left empty. In that case, you'd need to specify the absolute path for each call. When the base URL is set, you can use both relative and absolute path. + +```csharp +// Creates a client with default options to call a given base URL +var client = new RestClient("https://localhost:5000"); +``` + +#### Provide client options + +The most common way to create a client is to use the constructor with options. The options object has the type of `RestClientOptions`. +Here's an example of how to create a client using the same base path as in the previous sample, but with a couple additional settings: + +```csharp +// Creates a client using the options object +var options = new RestClientOptions("https://localhost:5000") { + MaxTimeout = 1000 +}; +var client = new RestClient(options); +``` + +#### Advanced configuration + +RestSharp can be configured with more tweaks, including default request options, how it should handle responses, how serialization works, etc. You can also provide your own instance of `HttpClient` or `HttpMessageHandler`. + +Read more about the advanced configuration of RestSharp on a [dedicated page](../advanced/configuration.md). + +## Simple factory + +Another way to create the client instance is to use a simple client factory. The factory will use the `BaseUrl` property of the client options to cache `HttpClient` instances. Every distinct base URL will get its own `HttpClient` instance. Other options don't affect the caching. Therefore, if you use different options for the same base URL, you'll get the same `HttpClient` instance, which will not be configured with the new options. Options that aren't applied _after_ the first client instance is created are: + +* `Credentials` +* `UseDefaultCredentials` +* `AutomaticDecompression` +* `PreAuthenticate` +* `FollowRedirects` +* `RemoteCertificateValidationCallback` +* `ClientCertificates` +* `MaxRedirects` +* `MaxTimeout` +* `UserAgent` +* `Expect100Continue` + +Constructor parameters to configure the `HttpMessageHandler` and default `HttpClient` headers configuration are also ignored for the cached instance as the factory only configures the handler once. + +You need to set the `useClientFactory` parameter to `true` in the `RestClient` constructor to enable the factory. + +```csharp +var client = new RestClient("https://api.twitter.com/2", true); +``` + +## Reusing HttpClient + +RestSharp uses `HttpClient` internally to make HTTP requests. It's possible to reuse the same `HttpClient` instance for multiple `RestClient` instances. This is useful when you want to share the same connection pool between multiple `RestClient` instances. + +One way of doing it is to use `RestClient` constructors that accept an instance of `HttpClient` or `HttpMessageHandler` as an argument. Note that in that case not all the options provided via `RestClientOptions` will be used. Here is the list of options that will work: + +- `BaseAddress` is be used to set the base address of the `HttpClient` instance if base address is not set there already. +- `MaxTimeout` is used to cancel the call using the cancellation token source, so +- `UserAgent` will be added to the `RestClient.DefaultParameters` list as a HTTP header. This will be added to each request made by the `RestClient`, and the `HttpClient` instance will not be modified. This is to allow the `HttpClient` instance to be reused for scenarios where different `User-Agent` headers are required. +- `Expect100Continue` + +Another option is to use a simple HTTP client factory as described [above](#simple-factory). + +## Blazor support + +Inside a Blazor webassembly app, you can make requests to external API endpoints. Microsoft examples show how to do it with `HttpClient`, and it's also possible to use RestSharp for the same purpose. + +You need to remember that webassembly has some platform-specific limitations. Therefore, you won't be able to instantiate `RestClient` using all of its constructors. In fact, you can only use `RestClient` constructors that accept `HttpClient` or `HttpMessageHandler` as an argument. If you use the default parameterless constructor, it will call the option-based constructor with default options. The options-based constructor will attempt to create an `HttpMessageHandler` instance using the options provided, and it will fail with Blazor, as some of those options throw thw "Unsupported platform" exception. + +Here is an example how to register the `RestClient` instance globally as a singleton: + +```csharp +builder.Services.AddSingleton(new RestClient(new HttpClient())); +``` + +Then, on a page you can inject the instance: + +```html +@page "/fetchdata" +@using RestSharp +@inject RestClient _restClient +``` + +And then use it: + +```csharp +@code { + private WeatherForecast[]? forecasts; + + protected override async Task OnInitializedAsync() { + forecasts = await _restClient.GetJsonAsync("http://localhost:5104/weather"); + } + + public class WeatherForecast { + public DateTime Date { get; set; } + public int TemperatureC { get; set; } + public string? Summary { get; set; } + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + } +} +``` + +In this case, the call will be made to a WebAPI server hosted at `http://localhost:5104/weather`. Remember that if the WebAPI server is not hosting the webassembly itself, it needs to have a CORS policy configured to allow the webassembly origin to access the API endpoint from the browser. diff --git a/docs/versioned_docs/version-v111/usage/example.md b/docs/versioned_docs/version-v111/usage/example.md new file mode 100644 index 000000000..3e1bc8ec2 --- /dev/null +++ b/docs/versioned_docs/version-v111/usage/example.md @@ -0,0 +1,152 @@ +--- +sidebar_position: 1 +--- + +# Example + +RestSharp works best as the foundation for a proxy class for your API. Each API would most probably require different settings for `RestClient`. Hence, a dedicated API class (and its interface) gives you sound isolation between different `RestClient` instances and make them testable. + +For example, let's look at a simple Twitter API v2 client, which uses OAuth2 machine-to-machine authentication. For it to work, you would need to have access to the Twitter Developers portal, a project, and an approved application inside the project with OAuth2 enabled. + +## Client model + +Before implementing an API client, we need to have a model for it. The model includes an abstraction for the client, which has functions for the API calls we are interested to implement. In addition, the client model would include the necessary request and response models. Usually those are simple classes or records without logic, which are often referred to as DTOs (data transfer objects). + +This example starts with a single function that retrieves one Twitter user. Let's begin by defining the API client interface: + +```csharp +public interface ITwitterClient { + Task GetUser(string user); +} +``` + +As the function returns a `TwitterUser` instance, we need to define it as a model: + +```csharp +public record TwitterUser(string Id, string Name, string Username); +``` + +## Client implementation + +When that is done, we can implement the interface and add all the necessary code blocks to get a working API client. + +The client class needs the following: +- A constructor for passing API credentials +- A wrapped `RestClient` instance with the Twitter API base URI pre-configured +- An authenticator to support authorizing the client using Twitter OAuth2 authentication +- The actual function to get the user (to implement the `ITwitterClient` interface) + +Creating an authenticator is described [below](#authenticator). + +Here's how the client implementation could look like: + +```csharp +public class TwitterClient : ITwitterClient, IDisposable { + readonly RestClient _client; + + public TwitterClient(string apiKey, string apiKeySecret) { + var options = new RestClientOptions("https://api.twitter.com/2"); + _client = new RestClient(options); + } + + public async Task GetUser(string user) { + var response = await _client.GetAsync>( + "users/by/username/{user}", + new { user } + ); + return response!.Data; + } + + record TwitterSingleObject(T Data); + + public void Dispose() { + _client?.Dispose(); + GC.SuppressFinalize(this); + } +} +``` + +It is also possible to use ASP.NET Core Options for configuring the client, instead of passing the credentials as strings. For example, we can add a class for Twitter client options, and use it in a constructor: + +```csharp +public class TwitterClientOptions(string ApiKey, string ApiSecret); + +public TwitterClient(IOptions options) { + var opt = new RestClientOptions("https://api.twitter.com/2"); + _client = new RestClient(opt); +} +``` + +Then, you can register and configure the client using ASP.NET Core dependency injection container. + +Right now, the client won't really work as Twitter API requires authentication. It's covered in the next section. + +## Authenticator + +Before we can call the API itself, we need to get a bearer token. Twitter exposes an endpoint `https://api.twitter.com/oauth2/token`. As it follows the OAuth2 conventions, the code can be used to create an authenticator for some other vendors. + +First, we need a model for deserializing the token endpoint response. OAuth2 uses snake case for property naming, so we need to decorate model properties with `JsonPropertyName` attribute: + +```csharp +record TokenResponse { + [JsonPropertyName("token_type")] + public string TokenType { get; init; } + [JsonPropertyName("access_token")] + public string AccessToken { get; init; } +} +``` + +Next, we create the authenticator itself. It needs the API key and API key secret to call the token endpoint using basic HTTP authentication. In addition, we can extend the list of parameters with the base URL to convert it to a more generic OAuth2 authenticator. + +The easiest way to create an authenticator is to inherit from the `AuthenticatorBase` base class: + +```csharp +public class TwitterAuthenticator : AuthenticatorBase { + readonly string _baseUrl; + readonly string _clientId; + readonly string _clientSecret; + + public TwitterAuthenticator(string baseUrl, string clientId, string clientSecret) : base("") { + _baseUrl = baseUrl; + _clientId = clientId; + _clientSecret = clientSecret; + } + + protected override async ValueTask GetAuthenticationParameter(string accessToken) { + Token = string.IsNullOrEmpty(Token) ? await GetToken() : Token; + return new HeaderParameter(KnownHeaders.Authorization, Token); + } +} +``` + +During the first call made by the client using the authenticator, it will find out that the `Token` property is empty. It will then call the `GetToken` function to get the token once and reuse the token going forward. + +Now, we need to implement the `GetToken` function in the class: + +```csharp +async Task GetToken() { + var options = new RestClientOptions(_baseUrl){ + Authenticator = new HttpBasicAuthenticator(_clientId, _clientSecret), + }; + using var client = new RestClient(options); + + var request = new RestRequest("oauth2/token") + .AddParameter("grant_type", "client_credentials"); + var response = await client.PostAsync(request); + return $"{response!.TokenType} {response!.AccessToken}"; +} +``` + +As we need to make a call to the token endpoint, we need our own short-lived instance of `RestClient`. Unlike the actual Twitter client, it will use the `HttpBasicAuthenticator` to send the API key and secret as the username and password. The client then gets disposed as we only use it once. + +Here we add a POST parameter `grant_type` with `client_credentials` as its value. At the moment, it's the only supported value. + +The POST request will use the `application/x-www-form-urlencoded` content type by default. + +::: note +Sample code provided on this page is a production code. For example, the authenticator might produce undesired side effect when multiple requests are made at the same time when the token hasn't been obtained yet. It can be solved rather than simply using semaphores or synchronized invocation. +::: + +## Final words + +This page demonstrates how an API client can be implemented as a typed, configurable client with its own interface. Usage of the client in applications is not covered here as different application types and target frameworks have their own idiomatic ways to use HTTP clients. diff --git a/docs/versioned_docs/version-v111/usage/execute.md b/docs/versioned_docs/version-v111/usage/execute.md new file mode 100644 index 000000000..36343f793 --- /dev/null +++ b/docs/versioned_docs/version-v111/usage/execute.md @@ -0,0 +1,172 @@ +--- +sidebar_position: 5 +title: Making calls +--- + +## Executing requests + +Once you've added all the parameters to your `RestRequest`, you are ready to make a request. + +`RestClient` has a single function for this: + +```csharp +public async Task ExecuteAsync( + RestRequest request, + CancellationToken cancellationToken = default +) +``` + +You can also avoid setting the request method upfront and use one of the overloads: + +```csharp +Task ExecuteGetAsync(RestRequest request, CancellationToken cancellationToken) +Task ExecutePostAsync(RestRequest request, CancellationToken cancellationToken) +Task ExecutePutAsync(RestRequest request, CancellationToken cancellationToken) +Task ExecuteDeleteAsync(RestRequest request, CancellationToken cancellationToken) +Task ExecuteHeadAsync(RestRequest request, CancellationToken cancellationToken) +Task ExecuteOptionsAsync(RestRequest request, CancellationToken cancellationToken) +``` + +When using any of those methods, you will get the response content as string in `response.Content`. + +RestSharp can deserialize the response for you. To use that feature, use one of the generic overloads: + +```csharp +Task> ExecuteAsync(RestRequest request, CancellationToken cancellationToken) +Task> ExecuteGetAsync(RestRequest request, CancellationToken cancellationToken) +Task> ExecutePostAsync(RestRequest request, CancellationToken cancellationToken) +Task> ExecutePutAsync(RestRequest request, CancellationToken cancellationToken) +Task> ExecuteDeleteAsync(RestRequest request, CancellationToken cancellationToken) +Task> ExecuteHeadAsync(RestRequest request, CancellationToken cancellationToken) +Task> ExecuteOptionsAsync(RestRequest request, CancellationToken cancellationToken) +``` + +:::note Beware of errors +All the overloads with names starting with `Execute` don't throw an exception if the server returns an error. Read more about it [here](../advanced/error-handling.md). +It allows you to inspect responses and handle remote server errors gracefully. Overloads without `Execute` prefix throw exceptions in case of any error, so you'd need to ensure to handle exceptions properly. +::: + +If you just need a deserialized response, you can use one of the extensions: + +```csharp +Task GetAsync(RestRequest request, CancellationToken cancellationToken) +Task PostAsync(RestRequest request, CancellationToken cancellationToken) +Task PutAsync(RestRequest request, CancellationToken cancellationToken) +Task PatchAsync(RestRequest request, CancellationToken cancellationToken) +Task DeleteAsync(RestRequest request, CancellationToken cancellationToken) +Task HeadAsync(RestRequest request, CancellationToken cancellationToken) +Task OptionsAsync(RestRequest request, CancellationToken cancellationToken) +``` + +Those extensions will throw an exception if the server returns an error, as there's no other way to float the error back to the caller. + +The `IRestClient` interface also has extensions for making requests without deserialization, which throw an exception if the server returns an error even if the client is configured to not throw exceptions. + +```csharp +Task GetAsync(RestRequest request, CancellationToken cancellationToken) +Task PostAsync(RestRequest request, CancellationToken cancellationToken) +Task PutAsync(RestRequest request, CancellationToken cancellationToken) +Task PatchAsync(RestRequest request, CancellationToken cancellationToken) +Task DeleteAsync(RestRequest request, CancellationToken cancellationToken) +Task HeadAsync(RestRequest request, CancellationToken cancellationToken) +Task OptionsAsync(RestRequest request, CancellationToken cancellationToken) +``` + +### Sync calls + +The preferred way for making requests is to execute them asynchronously as HTTP calls are IO-bound operations. +If you are unable to make async calls, all the functions about have sync overloads, which have the same names without `Async` suffix. +For example, for making a sync `GET` call you can use `ExecuteGet(request)` or `Get`, etc. + +## Requests without body + +Some HTTP methods don't suppose to be used with request body. For those methods, RestSharp supports making simplified calls without using `RestRequest`. All you need is to provide the resource path as a string. + +For example, you can make a `DELETE` call like this: + +```csharp +var response = await client.ExecuteDeleteAsync($"order/delete/{orderId}", cancellationToken); +``` + +Similarly, you can make `GET` calls with or without deserialization of the response using `ExecuteGetAsync(resource)`, `GetAsync(resource)`, `ExecuteGetAsync(resource)`, and `GetAsync(resource)` (see below). + +## JSON requests + +RestSharp provides an easier API for making calls to endpoints that accept and return JSON. + +### GET calls + +To make a simple `GET` call and get a deserialized JSON response with a pre-formed resource string, use this: + +```csharp +var response = await client.GetAsync("endpoint?foo=bar", cancellationToken); +``` + +:::note +In v111, `GetJsonAsync` is renamed to `GetAsync`. +::: + +You can also use a more advanced extension that uses an object to compose the resource string: + +```csharp +var client = new RestClient("https://example.org"); +var args = new { + id = "123", + foo = "bar" +}; +// Will make a call to https://example.org/endpoint/123?foo=bar +var response = await client.GetAsync("endpoint/{id}", args, cancellationToken); +``` + +It will search for the URL segment parameters matching any of the object properties and replace them with values. All the other properties will be used as query parameters. + +One note about `GetAsync` is that it will deserialize the response with any supported content type, not only JSON. + +### POST calls + +Similar things are available for `POST` requests. + +```csharp +var request = new CreateOrder("123", "foo", 10100); +// Will post the request object as JSON to "orders" and returns a +// JSON response deserialized to OrderCreated +var result = client.PostJsonAsync("orders", request, cancellationToken); +``` + +```csharp +var request = new CreateOrder("123", "foo", 10100); +// Will post the request object as JSON to "orders" and returns a +// status code, not expecting any response body +var statusCode = client.PostJsonAsync("orders", request, cancellationToken); +``` + +The same two extensions also exist for `PUT` requests (`PutJsonAsync`); + +## Downloading binary data + +There are two functions that allow you to download binary data from the remote API. + +First, there's `DownloadDataAsync`, which returns `Task`. This function allows you to open a stream reader and asynchronously stream large responses to memory or disk. + +## JSON streaming + +For HTTP API endpoints that stream the response data (like [Twitter search stream](https://developer.twitter.com/en/docs/twitter-api/tweets/filtered-stream/api-reference/get-tweets-search-stream)) you can use RestSharp with `StreamJsonAsync`, which returns an `IAsyncEnumerable`: + +```csharp +public async IAsyncEnumerable SearchStream( + [EnumeratorCancellation] CancellationToken cancellationToken = default +) { + var response = _client.StreamJsonAsync>( + "tweets/search/stream", cancellationToken + ); + + await foreach (var item in response.WithCancellation(cancellationToken)) { + yield return item.Data; + } +} +``` + +The main limitation of this function is that it expects each JSON object to be returned as a single line. It is unable to parse the response by combining multiple lines into a JSON string. + diff --git a/docs/versioned_docs/version-v111/usage/request.md b/docs/versioned_docs/version-v111/usage/request.md new file mode 100644 index 000000000..a844ed8a4 --- /dev/null +++ b/docs/versioned_docs/version-v111/usage/request.md @@ -0,0 +1,332 @@ +--- +sidebar_position: 4 +title: Preparing requests +--- + +## Create a request + +Before making a request using `RestClient`, you need to create a request instance: + +```csharp +var request = new RestRequest(resource); // resource is the sub-path of the client base path +``` + +The default request type is `GET` and you can override it by setting the `Method` property. You can also set the method using the constructor overload: + +```csharp +var request = new RestRequest(resource, Method.Post); +``` + +After you've created a `RestRequest`, you can add parameters to it. Below, you can find all the parameter types supported by RestSharp. + +## Request headers + +Adds the header parameter as an HTTP header that is sent along with the request. The header name is the parameter's name and the header value is the value. + +You can use one of the following request methods to add a header parameter: + +```csharp +AddHeader(string name, string value); +AddHeader(string name, T value); // value will be converted to string +AddOrUpdateHeader(string name, string value); // replaces the header if it already exists +``` + +For example: + +```csharp +var request = new RestRequest("/path").AddHeader("X-Key", someKey); +``` + +You can also add header parameters to the client, and they will be added to every request made by the client. This is useful for adding authentication headers, for example. + +```csharp +client.AddDefaultHeader(string name, string value); +``` + +:::warning Avoid setting Content-Type header +RestSharp will use the correct content type by default. Avoid adding the `Content-Type` header manually to your requests unless you are absolutely sure it is required. You can add a custom content type to the [body parameter](#request-body) itself. +::: + +## Get or Post parameters + +The default RestSharp parameter type is `GetOrPostParameter`. You can add `GetOrPost` parameter to the request using the `AddParameter` function: + +```csharp +request + .AddParameter("name1", "value1") + .AddParameter("name2", "value2"); +``` + +`GetOrPost` behaves differently based on the HTTP method. If you execute a `GET` call, RestSharp will append the parameters to the URL in the form `url?name1=value1&name2=value2`. + +On a `POST` or `PUT` requests, it depends on whether you have files attached to a request. +If not, the parameters will be sent as the body of the request in the form `name1=value1&name2=value2`. Also, the request will be sent as `application/x-www-form-urlencoded`. + +In both cases, name and value will automatically be URL-encoded, unless specified otherwise: + +```csharp +request.AddParameter("name", "Væ üé", false); // don't encode the value +``` + +If you have files, RestSharp will send a `multipart/form-data` request. Your parameters will be part of this request in the form: + +``` +Content-Disposition: form-data; name="parameterName" + +ParameterValue +``` + +You can also add `GetOrPost` parameter as a default parameter to the client. This will add the parameter to every request made by the client. + +```csharp +client.AddDefaultParameter("foo", "bar"); +``` + +It will work the same way as request parameters, except that it will be added to every request. + +## Query string + +`QueryString` works like `GetOrPost`, except that it always appends the parameters to the url in the form `url?name1=value1&name2=value2`, regardless of the request method. + +Example: + +```csharp +var client = new RestClient("https://search.me"); +var request = new RestRequest("search") + .AddParameter("foo", "bar"); +var response = await client.GetAsync(request); +``` + +It will send a `GET` request to `https://search.me/search?foo=bar`. + +For `POST`-style requests you need to add the query string parameter explicitly: + +```csharp +request.AddQueryParameter("foo", "bar"); +``` + +In some cases, you might need to prevent RestSharp from encoding the query string parameter. +To do so, set the `encode` argument to `false` when adding the parameter: + +```csharp +request.AddQueryParameter("foo", "bar/fox", false); +``` + +You can also add a query string parameter as a default parameter to the client. This will add the parameter to every request made by the client. + +```csharp +client.AddDefaultQueryParameter("foo", "bar"); +``` + +The line above will result in all the requests made by that client instance to have `foo=bar` in the query string for all the requests made by that client. + +## Using AddObject + +You can avoid calling `AddParameter` multiple times if you collect all the parameters in an object, and then use `AddObject`. +For example, this code: + +```csharp +var params = new { + status = 1, + priority = "high", + ids = new [] { "123", "456" } +}; +request.AddObject(params); +``` + +is equivalent to: + +```csharp +request.AddParameter("status", 1); +request.AddParameter("priority", "high"); +request.AddParameter("ids", "123,456"); +``` + +Remember that `AddObject` only works if your properties have primitive types. It also works with collections of primitive types as shown above. + +If you need to override the property name or format, you can do it using the `RequestProperty` attribute. For example: + +```csharp +public class RequestModel { + // override the name and the format + [RequestProperty(Name = "from_date", Format = "d")] + public DateTime FromDate { get; set; } +} + +// add it to the request +request.AddObject(new RequestModel { FromDate = DateTime.Now }); +``` + +In this case, the request will get a GET or POST parameter named `from_date` and its value would be the current date in short date format. + +## Using AddObjectStatic + +Request function `AddObjectStatic(...)` allows using pre-compiled expressions for getting property values. Compared to `AddObject` that uses reflections for each call, `AddObjectStatic` caches functions to retrieve properties from an object of type `T`, so it works much faster. + +You can instruct `AddObjectStatic` to use custom parameter names and formats, as well as supply the list of properties than need to be used as parameters. The last option could be useful if the type `T` has properties that don't need to be sent with HTTP call. + +To use custom parameter name or format, use the `RequestProperty` attribute. For example: + +```csharp +class TestObject { + [RequestProperty(Name = "some_data")] + public string SomeData { get; set; } + + [RequestProperty(Format = "d")] + public DateTime SomeDate { get; set; } + + [RequestProperty(Name = "dates", Format = "d")] + public DateTime[] DatesArray { get; set; } + + public int Plain { get; set; } + public DateTime[] PlainArray { get; set; } +} +``` + +## URL segment parameter + +Unlike `GetOrPost`, URL segment parameter replaces placeholder values in the request URL: + +```csharp +var request = new RestRequest("health/{entity}/status") + .AddUrlSegment("entity", "s2"); +``` + +When the request executes, RestSharp will try to match any `{placeholder}` with a parameter of that name (without the `{}`) and replace it with the value. So the above code results in `health/s2/status` being the URL. + +You can also add `UrlSegment` parameter as a default parameter to the client. This will add the parameter to every request made by the client. + +```csharp +client.AddDefaultUrlSegment("foo", "bar"); +``` + +## Cookies + +You can add cookies to a request using the `AddCookie` method: + +```csharp +request.AddCookie("foo", "bar"); +``` + +RestSharp will add cookies from the request as cookie headers and then extract the matching cookies from the response. You can observe and extract response cookies using the `RestResponse.Cookies` properties, which has the `CookieCollection` type. + +However, the usage of a default URL segment parameter is questionable as you can just include the parameter value to the base URL of the client. There is, however, a `CookieContainer` instance on the request level. You can either assign the pre-populated container to `request.CookieContainer`, or let the container be created by the request when you call `AddCookie`. Still, the container is only used to extract all the cookies from it and create cookie headers for the request instead of using the container directly. It's because the cookie container is normally configured on the `HttpClientHandler` level and cookies are shared between requests made by the same client. In most of the cases this behaviour can be harmful. + +If your use case requires sharing cookies between requests made by the client instance, you can use the client-level `CookieContainer`, which you must provide as the options' property. You can add cookies to the container using the container API. No response cookies, however, would be auto-added to the container, but you can do it in code by getting cookies from the `Cookes` property of the response and adding them to the client-level container available via `IRestClient.Options.CookieContainer` property. + +## Request Body + +RestSharp supports multiple ways to add a request body: +- `AddJsonBody` for JSON payloads +- `AddXmlBody` for XML payloads +- `AddStringBody` for pre-serialized payloads + +We recommend using `AddJsonBody` or `AddXmlBody` methods instead of `AddParameter` with type `BodyParameter`. Those methods will set the proper request type and do the serialization work for you. + +When you make a `POST`, `PUT` or `PATCH` request and added `GetOrPost` [parameters](#get-or-post-parameters), RestSharp will send them as a URL-encoded form request body by default. When a request also has files, it will send a `multipart/form-data` request. You can also instruct RestSharp to send the body as `multipart/form-data` by setting the `AlwaysMultipartFormData` property to `true`. + +You can specify a custom body content type if necessary. The `contentType` argument is available in all the overloads that add a request body. + +It is not possible to add client-level default body parameters. + +### String body + +If you have a pre-serialized payload like a JSON string, you can use `AddStringBody` to add it as a body parameter. You need to specify the content type, so the remote endpoint knows what to do with the request body. For example: + +```csharp +const json = "{ data: { foo: \"bar\" } }"; +request.AddStringBody(json, ContentType.Json); +``` + +### JSON body + +When you call `AddJsonBody`, it does the following for you: + +- Instructs the RestClient to serialize the object parameter as JSON when making a request +- Sets the content type to `application/json` +- Sets the internal data type of the request body to `DataType.Json` + +Here is the example: + +```csharp +var param = new MyClass { IntData = 1, StringData = "test123" }; +request.AddJsonBody(param); +``` + +It is possible to override the default content type by supplying the `contentType` argument. For example: + +```csharp +request.AddJsonBody(param, "text/x-json"); +``` + +If you use a pre-serialized string with `AddJsonBody`, it will be sent as-is. The `AddJsonBody` will detect if the parameter is a string and will add it as a string body with JSON content type. +Essentially, it means that top-level strings won't be serialized as JSON when you use `AddJsonBody`. To overcome this issue, you can use an overload of `AddJsonBody`, which allows you to tell RestSharp to serialize the string as JSON: + +```csharp +const string payload = @" +""requestBody"": { + ""content"": { + ""application/json"": { + ""schema"": { + ""type"": ""string"" + } + } + } +},"; +request.AddJsonBody(payload, forceSerialize: true); // the string will be serialized +request.AddJsonBody(payload); // the string will NOT be serialized and will be sent as-is +``` + +### XML body + +When you call `AddXmlBody`, it does the following for you: + +- Instructs the RestClient to serialize the object parameter as XML when making a request +- Sets the content type to `application/xml` +- Sets the internal data type of the request body to `DataType.Xml` + +:::warning +Do not send XML string to `AddXmlBody`; it won't work! +::: + + +## Uploading files + +To add a file to the request you can use the `RestRequest` function called `AddFile`. The main function accepts the `FileParameter` argument: + +```csharp +request.AddFile(fileParameter); +``` + +You can instantiate the file parameter using `FileParameter.Create` that accepts a bytes array, or `FileParameter.FromFile`, which will load the file from disk. + +There are also extension functions that wrap the creation of `FileParameter` inside: + +```csharp +// Adds a file from disk +AddFile(parameterName, filePath, contentType); + +// Adds an array of bytes +AddFile(parameterName, bytes, fileName, contentType); + +// Adds a stream returned by the getFile function +AddFile(parameterName, getFile, fileName, contentType); +``` + +Remember that `AddFile` will set all the necessary headers, so please don't try to set content headers manually. + +You can also provide file upload options to the `AddFile` call. The options are: +- `DisableFilenameEncoding` (default `false`): if set to `true`, RestSharp will not encode the file name in the `Content-Disposition` header +- `DisableFilenameStar` (default `true`): if set to `true`, RestSharp will not add the `filename*` parameter to the `Content-Disposition` header + +Example of using the options: + +```csharp +var options = new FileParameterOptions { + DisableFilenameEncoding = true, + DisableFilenameStar = false +}; +request.AddFile("file", filePath, options: options); +``` + +The options specified in the snippet above usually help when you upload files with non-ASCII characters in their names. diff --git a/docs/versioned_docs/version-v111/usage/response.md b/docs/versioned_docs/version-v111/usage/response.md new file mode 100644 index 000000000..d9cd10626 --- /dev/null +++ b/docs/versioned_docs/version-v111/usage/response.md @@ -0,0 +1,36 @@ +--- +sidebar_position: 6 +title: Handling responses +--- + +All `Execute{Method}Async` functions return an instance of `RestResponse`. Similarly, `Execute{Method}Async` return a generic instance of `RestResponse` where `T` is the response object type. + +Response object contains the following properties: + +| Property | Type | Description | +|--------------------------|-----------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------| +| `Request` | `RestRequest` | Request instance that was used to get the response. | +| `ContentType` | `string?` | Response content type. `Null` if response has no content. | +| `ContentLength` | `long?` | Response content length. `Null` if response has no content. | +| `ContentEncoding` | `ICollection` | Content encoding collection. Empty if response has no content. | +| `Content` | `string?` | Response content as string. `Null` if response has no content. | +| `IsSuccessfulStatusCode` | `bool` | Indicates if response was successful, so no errors were reported by the server. Note that `404` response code means success. | +| `ResponseStatus` | `None`, `Completed`, `Error`, `TimedOut`, `Aborted` | Response completion status. Note that completed responses might still return errors. | +| `IsSuccessful` | `bool` | `True` when `IsSuccessfulStatusCode` is `true` and `ResponseStatus` is `Completed`. | +| `StatusDescription` | `string?` | Response status description, if available. | +| `RawBytes` | `byte[]?` | Response content as byte array. `Null` if response has no content. | +| `ResponseUri` | `Uri?` | URI of the response, which might be different from request URI in case of redirects. | +| `Server` | `string?` | Server header value of the response. | +| `Cookies` | `CookieCollection?` | Collection of cookies received with the response, if any. | +| `Headers` | Collection of `HeaderParameter` | Response headers. | +| `ContentHeaders` | Collection of `HeaderParameter` | Response content headers. | +| `ErrorMessage` | `string?` | Transport or another non-HTTP error generated while attempting request. | +| `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. | + +In addition, `RestResponse` has one additional property: + +| Property | Type | Description | +|----------|------|-----------------------------------------------------------------------------------------------------------------------------------------------------------| +| `Data` | `T?` | Deserialized response object. `Null` if there's no content in the response, deserializer failed to understand the response content, or if request failed. | diff --git a/docs/versioned_docs/version-v112/advanced/_category_.json b/docs/versioned_docs/version-v112/advanced/_category_.json new file mode 100644 index 000000000..f395bdfe4 --- /dev/null +++ b/docs/versioned_docs/version-v112/advanced/_category_.json @@ -0,0 +1,7 @@ +{ + "label": "Advanced topics", + "position": 4, + "link": { + "type": "generated-index" + } +} diff --git a/docs/versioned_docs/version-v112/advanced/authenticators.md b/docs/versioned_docs/version-v112/advanced/authenticators.md new file mode 100644 index 000000000..14196d97c --- /dev/null +++ b/docs/versioned_docs/version-v112/advanced/authenticators.md @@ -0,0 +1,185 @@ +# Authenticators + +RestSharp includes authenticators for basic HTTP, OAuth1 and token-based (JWT and OAuth2). + +There are two ways to set the authenticator: client-wide or per-request. + +Set the client-wide authenticator by assigning the `Authenticator` property of `RestClientOptions`: + +```csharp +var options = new RestClientOptions("https://example.com") { + Authenticator = new HttpBasicAuthenticator("username", "password") +}; +var client = new RestClient(options); +``` + +To set the authenticator per-request, assign the `Authenticator` property of `RestRequest`: + +```csharp +var request = new RestRequest("/api/users/me") { + Authenticator = new HttpBasicAuthenticator("username", "password") +}; +var response = await client.ExecuteAsync(request, cancellationToken); +``` + +## Basic authentication + +The `HttpBasicAuthenticator` allows you pass a username and password as a basic `Authorization` header using a base64 encoded string. + +```csharp +var options = new RestClientOptions("https://example.com") { + Authenticator = new HttpBasicAuthenticator("username", "password") +}; +var client = new RestClient(options); +``` + +## OAuth1 + +For OAuth1 authentication the `OAuth1Authenticator` class provides static methods to help generate an OAuth authenticator. +OAuth1 authenticator will add the necessary OAuth parameters to the request, including signature. + +The authenticator will use `HMAC SHA1` to create a signature by default. +Each static function to create the authenticator allows you to override the default and use another method to generate the signature. + +### Request token + +Getting a temporary request token is the usual first step in the 3-legged OAuth1 flow. +Use `OAuth1Authenticator.ForRequestToken` function to get the request token authenticator. +This method requires a `consumerKey` and `consumerSecret` to authenticate. + +```csharp +var options = new RestClientOptions("https://api.twitter.com") { + Authenticator = OAuth1Authenticator.ForRequestToken(consumerKey, consumerSecret) +}; +var client = new RestClient(options); +var request = new RestRequest("oauth/request_token"); +``` + +The response should contain the token and the token secret, which can then be used to complete the authorization process. +If you need to provide the callback URL, assign the `CallbackUrl` property of the authenticator to the callback destination. + +### Access token + +Getting an access token is the usual third step in the 3-legged OAuth1 flow. +This method retrieves an access token when provided `consumerKey`, `consumerSecret`, `oauthToken`, and `oauthTokenSecret`. +If you don't have a token for this call, you need to make a call to get the request token as described above. + +```csharp +var authenticator = OAuth1Authenticator.ForAccessToken( + consumerKey, consumerSecret, oauthToken, oauthTokenSecret +); +var options = new RestClientOptions("https://api.twitter.com") { + Authenticator = authenticator +}; +var client = new RestClient(options); +var request = new RestRequest("oauth/access_token"); +``` + +If the second step in 3-leg OAuth1 flow returned a verifier value, you can use another overload of `ForAccessToken`: + +```csharp +var authenticator = OAuth1Authenticator.ForAccessToken( + consumerKey, consumerSecret, oauthToken, oauthTokenSecret, verifier +); +``` + +The response should contain the access token that can be used to make calls to protected resources. + +For refreshing access tokens, use one of the two overloads of `ForAccessToken` that accept `sessionHandle`. + +### Protected resource + +When the access token is available, use `ForProtectedResource` function to get the authenticator for accessing protected resources. + +```csharp +var authenticator = OAuth1Authenticator.ForAccessToken( + consumerKey, consumerSecret, accessToken, accessTokenSecret +); +var options = new RestClientOptions("https://api.twitter.com/1.1") { + Authenticator = authenticator +}; +var client = new RestClient(options); +var request = new RestRequest("statuses/update.json", Method.Post) + .AddParameter("status", "Hello Ladies + Gentlemen, a signed OAuth request!") + .AddParameter("include_entities", "true"); +``` + +### xAuth + +xAuth is a simplified version of OAuth1. It allows sending the username and password as `x_auth_username` and `x_auth_password` request parameters and directly get the access token. xAuth is not widely supported, but RestSharp still allows using it. + +Create an xAuth authenticator using `OAuth1Authenticator.ForClientAuthentication` function: + +```csharp +var authenticator = OAuth1Authenticator.ForClientAuthentication( + consumerKey, consumerSecret, username, password +); +``` + +### 0-legged OAuth + +The access token authenticator can be used in 0-legged OAuth scenarios by providing `null` for the `consumerSecret`. + +```csharp +var authenticator = OAuth1Authenticator.ForAccessToken( + consumerKey, null, oauthToken, oauthTokenSecret +); +``` + +## OAuth2 + +RestSharp has two very simple authenticators to send the access token as part of the request. + +`OAuth2UriQueryParameterAuthenticator` accepts the access token as the only constructor argument, and it will send the provided token as a query parameter `oauth_token`. + +`OAuth2AuthorizationRequestHeaderAuthenticator` has two constructors. One only accepts a single argument, which is the access token. The other constructor also allows you to specify the token type. The authenticator will then add an `Authorization` header using the specified token type or `OAuth` as the default token type, and the token itself. + +For example: + +```csharp +var authenticator = new OAuth2AuthorizationRequestHeaderAuthenticator( + token, "Bearer" +); +var options = new RestClientOptions("https://example.com") { + Authenticator = authenticator +}; +var client = new RestClient(options); +``` + +The code above will tell RestSharp to send the bearer token with each request as a header. Essentially, the code above does the same as the sample for `JwtAuthenticator` below. + +As those authenticators don't do much to get the token itself, you might be interested in looking at our [sample OAuth2 authenticator](../usage/example.md#authenticator), which requests the token on its own. + +## JWT + +The JWT authentication can be supported by using `JwtAuthenticator`. It is a very simple class that can be constructed like this: + +```csharp +var authenticator = new JwtAuthenticator(myToken); +var options = new RestClientOptions("https://example.com") { + Authenticator = authenticator +}; +var client = new RestClient(options); +``` + +For each request, it will add an `Authorization` header with the value `Bearer `. + +As you might need to refresh the token from, you can use the `SetBearerToken` method to update the token. + +## Custom authenticator + +You can write your own implementation by implementing `IAuthenticator` and +registering it with your RestClient: + +```csharp +var authenticator = new SuperAuthenticator(); // implements IAuthenticator +var options = new RestClientOptions("https://example.com") { + Authenticator = authenticator +}; +var client = new RestClient(options); +``` + +The `Authenticate` method is the very first thing called upon calling `RestClient.Execute` or `RestClient.Execute`. +It gets the `RestRequest` currently being executed giving you access to every part of the request data (headers, parameters, etc.) + +You can find an example of a custom authenticator that fetches and uses an OAuth2 bearer token [here](../usage/example.md#authenticator). diff --git a/docs/versioned_docs/version-v112/advanced/configuration.md b/docs/versioned_docs/version-v112/advanced/configuration.md new file mode 100644 index 000000000..b57638632 --- /dev/null +++ b/docs/versioned_docs/version-v112/advanced/configuration.md @@ -0,0 +1,230 @@ +--- +title: Configuration +description: Learn how to configure RestClient for non-trivial use cases. +sidebar_position: 1 +--- + +# Configuring RestClient + +This page describes how to create and configure `RestClient`. + +## Basic configuration + +The primary `RestClient` constructor accepts an instance of `RestClientOptions`. Most of the time, default option values don't need to be changed. However, in some cases, you'd want to configure the client differently, so you'd need to change some of the options in your code. The constructor also contains a few optional parameters for additional configuration that is not covered by client options. Here's the constructor signature: + +```csharp +public RestClient( + RestClientOptions options, + ConfigureHeaders? configureDefaultHeaders = null, + ConfigureSerialization? configureSerialization = null, + bool useClientFactory = false +) +``` + +Constructor parameters are: + +| Name | Description | Mandatory | +|-------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------| +| options | Client options | Yes | +| configureDefaultHeaders | Function to configure headers. Allows to configure default headers for `HttpClient`. Most of the time you'd prefer using `client.AddDefaultHeader` instead. | No | +| configureSerialization | Function to configure client serializers with non-default options or to use a different serializer ([learn more](serialization.md)) | No | +| useClientFactory | Instructs the client to use `SimpleFactory` ([learn more](../usage/client.md#simple-factory)) to get an `HttpClient` instance | No | + +Here's an example of how to create a client using client options: + +```csharp +var options = new RestClientOptions("https://localhost:5000/api") { + DisableCharset = true +}; +var client = new RestClient(options); +``` + +When you only need to set the base URL, you can use a simplified constructor: + +```csharp +var client = new RestClient("https://localhost:5000/api"); +``` + +The simplified constructor will create an instance of client options and set the base URL provided as the constructor argument. + +Finally, you can override properties of default options using a configuration function. Here's the constructor signature that supports this method: + +```csharp +public RestClient( + ConfigureRestClient? configureRestClient = null, + ConfigureHeaders? configureDefaultHeaders = null, + ConfigureSerialization? configureSerialization = null, + bool useClientFactory = false +) +``` + +For example: + +```csharp +var client = new RestClient(options => { + options.BaseUrl = new Url("https://localhost:5000/api"), + options.DisableCharset = true +}); +``` + +You can also provide the base URL as a constructor argument like this: + +```csharp +var client = new RestClient("https://localhost:5000/api", options => { + options.DisableCharset = true +}); +``` + +## Using custom HttpClient + +By default, RestSharp creates an instance of `HttpClient` configured using the client options, and keeps it during the lifetime of the client. When the `RestClient` instance gets disposed, it also disposes the `HttpClient` instance. + +There might be a case when you need to provide your own `HttpClient`. For example, you would want to use `HttpClient` created by HTTP client factory. RestSharp allows you to do it by using additional constructors. These constructors are: + +```csharp +// Create a client using an existing HttpClient and RestClientOptions (optional) +public RestClient( + HttpClient httpClient, + RestClientOptions? options, + bool disposeHttpClient = false, + ConfigureSerialization? configureSerialization = null +) + +// Create a client using an existing HttpClient and optional RestClient configuration function +public RestClient( + HttpClient httpClient, + bool disposeHttpClient = false, + ConfigureRestClient? configureRestClient = null, + ConfigureSerialization? configureSerialization = null +) +``` + +The `disposeHttpClient` argument tells the client to dispose `HttpClient` when the client itself gets disposed. It's set to `false` by default as when the `HttpClient` is provided from the outside, it should normally be disposed on the outside as well. + +## Using custom message handler + +Unless you use an external instance of `HttpClient`, the `RestClient` creates one when being constructed, and it will use the default HTTP message handler, configured using `RestClientOptions`. Normally, you'd get a `SocketHttpHandler` with modern .NET, and `WinHttpHandler` with .NET Framework. + +There might be a case when you need to configure the HTTP message handler. For example, you want to add a delegating message handler. RestSharp allows you to do it by using additional constructors. There's one constructor that allows you to pass the custom `HttpMessageHandler`: + +```csharp +public RestClient( + HttpMessageHandler handler, + bool disposeHandler = true, + ConfigureRestClient? configureRestClient = null, + ConfigureSerialization? configureSerialization = null +) +``` + +This constructor will create a new `HttpClient` instance using the provided message handler. As RestSharp will dispose the `HttpClient` instance when the `RestClient` instance gets disposed, the handler will be disposed as well. If you want to change that and keep the handler, set the `disposeHandler` parameter to `false`. + +:::note +When using a custom message handler, RestSharp **will not** configure it with client options, which are normally used to configure the handler created by RestSharp. +::: + +Another way to customize the message handler is to allow RestSharp to create a handler, but then configure it, or wrap it in a delegating handler. It can be done by using the `RestClientOptions.ConfigureMessageHandler` property. It can be set to a function that receives the handler created by RestSharp and returned either the same handler with different settings, or a new handler. + +For example, if you want to use `MockHttp` and its handler for testing, you can do it like this: + +```csharp +var mockHttp = new MockHttpMessageHandler(); +// Configure the MockHttp handler to do the checks +... + +var options = new RestClientOptions(Url) { + ConfigureMessageHandler = _ => mockHttp +}; +using var client = new RestClient(options); +``` + +In this example, we are reassigning the handler to MockHttp, so the handler created by RestSharp isn't used. In other cases you want to use delegating handlers as middleware, so you'd pass the handler created by RestSharp to the delegating handler: + +```csharp +var options = new RestClientOptions(Url) { + ConfigureMessageHandler = handler => new MyDelegatingHandler(handler) +}; +using var client = new RestClient(options); +``` + +## Client options + +RestSharp allows configuring `RestClient` using client options, as mentioned at the beginning of this page. Below, you find more details about available options. + +| Option | Description | +|----------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `BaseUrl` | Client base URL. It can also be provided as the `RestClientOptions` constructor argument. | +| `ConfigureMessageHandler` | Configures the HTTP message handler (see above). | +| `CalculateResponseStatus` | Function to calculate a different response status from `HttpResponseMessage`. By default, the request is considered as complete if it returns a successful status code or 404. | +| `Authenticator` | Client-level authenticator. Read more about authenticators [here](authenticators.md). | +| `Interceptors` | A collector of interceptors. Read more about interceptors [here](interceptors.md). | +| `Credentials` | Instance of `ICredentials` used for NTLM or Kerberos authentication. Not supported in browsers. | +| `UseDefaultCredentials` | Whether to use default OS credentials for NTLM or Kerberos authentication. Not supported in browsers. | +| `DisableCharset` | When set to `true`, the `Content-Type` header won't have the `charset` portion. Some older web servers don't understand the `charset` portion in the header and fail to process the request. | +| `AutomaticDecompression` | Allows customizing supported decompression methods. Default is `All` except for .NET Framework that only support `GZip`. Not supported in browsers. | +| `MaxRedirects` | The number of redirects to follow. Not supported in browsers. | +| `ClientCertificates` | A collection of X.509 client certificates to be used for authentication. Not supported in browsers. | +| `Proxy` | Can be used if the client needs to use an explicit, non-default proxy. Not supported in browsers, on iOS and tvOS. | +| `CachePolicy` | Shortcut for setting the default value for `Cache-Control` header. | +| `FollowRedirects` | Instructs the client to follow redirects. Default is `true`. | +| `Expect100Continue` | Gets or sets a value that indicates if the `Expect` header for an HTTP request contains `Continue`. | +| `UserAgent` | Allows overriding the default value for `User-Agent` header, which is `RestSharp/{version}`. | +| `PreAuthenticate` | Gets or sets a value that indicates whether the client sends an `Authorization` header with the request. Not supported in browsers. | +| `RemoteCertificateValidationCallback` | Custom function to validate the server certificate. Normally, it's used when the server uses a certificate that isn't trusted by default. | +| `BaseHost` | Value for the `Host` header sent with each request. | +| `CookieContainer` | Custom cookie container that will be shared among all calls made by the client. Normally not required as RestSharp handles cookies without using a client-level cookie container. | +| `MaxTimeout` | Client-level timeout in milliseconds. If the request timeout is also set, this value isn't used. | +| `Encoding` | Default request encoding. Override it only if you don't use UTF-8. | +| `ThrowOnDeserializationError` | Forces the client to throw if it fails to deserialize the response. Remember that not all deserialization issues forces the serializer to throw. Default is `false`, so the client will return a `RestResponse` with deserialization exception details. Only relevant for `Execute...` functions. | +| `FailOnDeserializationError` | When set to `true`, if the client fails to deserialize the response, the response object will have status `Failed`, although the HTTP calls might have been successful. Default is `true`. | +| `ThrowOnAnyError` | When set to `true`, the client will re-throw any exception from `HttpClient`. Default is `false`. Only applies for `Execute...` functions. | +| `AllowMultipleDefaultParametersWithSameName` | By default, adding parameters with the same name is not allowed. You can override this behaviour by setting this property to `true`. | +| `Encode` | A function to encode URLs, the default is a custom RestSharp function based on `Uri.EscapeDataString()`. Set it if you need a different way to do the encoding. | +| `EncodeQuery` | A function to encode URL query parameters. The default is the same function as for `Encode` property. | + +Some of the options are used by RestSharp code, but some are only used to configure the `HttpMessageHandler`. These options are: +- `Credentials` +- `UseDefaultCredentials` +- `AutomaticDecompression` +- `PreAuthenticate` +- `MaxRedirects` +- `RemoteCertificateValidationCallback` +- `ClientCertificates` +- `FollowRedirects` +- `Proxy` + +:::note +If setting these options to non-default values produce no desirable effect, check if your framework and platform supports them. RestSharp doesn't change behaviour based on values of those options. +::: + +The `IRestClient` interface exposes the `Options` property, so any option can be inspected at runtime. However, RestSharp converts the options object provided to the client constructor to an immutable object. Therefore, no client option can be changed after the client is instantiated. It's because changing client options at runtime can produce issues in concurrent environments, effectively rendering the client as not thread-safe. Apart from that, changing the options that are used to create the message handler would require re-creating the handler, and also `HttpClient`, which should not be done at runtime. + +## Configuring requests + +Client options apply to all requests made by the client. Sometimes, you want to fine-tune particular requests, so they execute with custom configuration. It's possible to do using properties of `RestRequest`, described below. + +| Name | Description | +|------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `AlwaysMultipartFormData` | When set to `true`, the request will be sent as a multipart form, even though it's not required. By default, RestSharp only sends requests with multiple attachments as multipart forms. Default is `false`. | +| `AlwaysSingleFileAsContent` | When set to true, the request with file attachment will not be sent as a multipart form, but as plain content. Default is `false`. It cannot be set to `true` when `AlwaysMultipartFormData` is set to `true`, or when the request has `POST` parameters. | +| `MultipartFormQuoteBoundary` | Default is `true`, which means that the form boundary string will be wrapped in quotes. If the server has an issue with that, setting this to `false` will remove quotes around the boundary. | +| `FormBoundary` | Allows specifying a custom multipart form boundary instead of using the default random string. | +| `RequestParameters` | Collection of request parameters. Normally, you won't need to use it as parameters are added to the request using `Add...` functions. | +| `CookieContainer` | Custom request-level cookie container. Default is `null`. You can still set request cookies using `AddCookie` and get response cookies from the response object without using cooking container. | +| `Authenticator` | Overrides the client-level authenticator. | +| `Files` | Collection of file parameters, read-only. Use `AddFile` for adding files to the request. | +| `Method` | Request HTTP method, default is `GET`. Only needed when using `Execute` or `ExecuteAsync` as other functions like `ExecutePostAsync` will override the request method. | +| `TImeout` | Overrides the client-level timeout. | +| `Resource` | Resource part of the remote endpoint URL. For example, when using the client-level base URL `https://localhost:5000/api` and `Resource` set to `weather`, the request will be sent to `https://localhost:5000/api/weather`. It can container resource placeholders to be used in combination with `AddUrlSegment` | +| `RequestFormat` | Identifies the request as JSON, XML, binary, or none. Rarely used because the client will set the request format based on the body type if functions like `AddJsonBody` or `AddXmlBody` are used. | +| `RootElement` | Used by the default deserializers to determine where to start deserializing from. Only supported for XML responses. Does not apply to requests. | +| `OnBeforeDeserialization` | **Obsolete** A function to be called before the response is deserializer. Allows changing the content before calling the deserializer. Use [interceptors](interceptors.md) instead. | +| `OnBeforeRequest` | **Obsolete** A function to be called right before the request is executed by `HttpClient`. It receives an instance of `HttpRequestMessage`. Use [interceptors](interceptors.md) instead. | +| `OnAfterRequest` | **Obsolete** A function to be called right after the request is executed by `HttpClient`. It receives an instance of `HttpResponseMessage`. Use [interceptors](interceptors.md) instead. | +| `Attempts` | When the request is being resent to retry, the property value increases by one. | +| `CompletionOption` | Instructs the client on when it should consider the request to be completed. The default is `ResponseContentRead`. It is automatically changed to `ResponseHeadersRead` when using async download functions or streaming. | +| `CachePolicy` | Overrides the client cache policy. | +| `ResponseWriter` | Allows custom handling of the response stream. The function gets the raw response stream and returns another stream or `null`. Cannot be used in combination with `AdvancedResponseWriter`. | +| `AdvancedResponseWriter` | Allows custom handling of the response. The function gets an instance of `HttpResponseMessage` and an instance of `RestRequest`. It must return an instance of `RestResponse`, so it effectively overrides RestSharp default functionality for creating responses. | +| `Interceptors` | Allows adding interceptors to the request. Both client-level and request-level interceptors will be called. | + +The table below contains all configuration properties of `RestRequest`. To learn more about adding request parameters, check the [usage page](../usage/request.md) page about creating requests with parameters. diff --git a/docs/versioned_docs/version-v112/advanced/error-handling.md b/docs/versioned_docs/version-v112/advanced/error-handling.md new file mode 100644 index 000000000..80e822dcc --- /dev/null +++ b/docs/versioned_docs/version-v112/advanced/error-handling.md @@ -0,0 +1,71 @@ +# Error handling + +If there is a network transport error (network is down, failed DNS lookup, etc.), or any kind of server error (except 404), `RestResponse.ResponseStatus` will be set to `ResponseStatus.Error`, otherwise it will be `ResponseStatus.Completed`. + +If an API returns a 404, `ResponseStatus` will still be `Completed`. If you need access to the HTTP status code returned, you will find it at `RestResponse.StatusCode`. +The `Status` property is an indicator of completion independent of the API error handling. + +Normally, RestSharp doesn't throw an exception if the request fails. + +However, it is possible to configure RestSharp to throw in different situations when it normally doesn't throw +in favor of giving you the error as a property. + +| Property | Behavior | +|-------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `FailOnDeserializationError` | Changes the default behavior when failed deserialization results in a successful response with an empty `Data` property of the response. Setting this property to `true` will tell RestSharp to consider failed deserialization as an error and set the `ResponseStatus` to `Error` accordingly. | +| `ThrowOnDeserializationError` | Changes the default behavior when failed deserialization results in empty `Data` property of the response. Setting this property to `true` will tell RestSharp to throw when deserialization fails. | +| `ThrowOnAnyError` | Setting this property to `true` changes the default behavior and forces RestSharp to throw if any errors occurs when making a request or during deserialization. | + +Those properties are available for the `RestClientOptions` and will be used for all request made with the client instance. + +For example, you can configure the client to throw an exception if any error occurs when making a request or when a request returns a non-successful HTTP status code: + +```csharp +var options = new RestClientOptions(url) { + ThrowOnAnyError = true +}; +var client = new RestClient(options); +var request = new RestRequest("resource/{id}").AddUrlSegment("id", 123); + +// 👇 will throw if the request fails +var deserialized = await client.GetAsync(request); + +// 👇 will NOT throw if the request fails, inspect the response to find out what happened +var response = await client.ExecuteGetAsync(request); +``` + +:::warning +Please be aware that deserialization failures will only work if the serializer throws an exception when deserializing the response. +Many serializers don't throw by default, and just return a `null` result. RestSharp is unable to figure out why `null` is returned, so it won't fail in this case. +Check the serializer documentation to find out if it can be configured to throw on deserialization error. +::: + +There are also slight differences on how different overloads handle exceptions. + +Asynchronous generic methods `GetAsync`, `PostAsync` and so on, which aren't a part of `RestClient` API (those methods are extension methods) return `Task`. It means that there's no `RestResponse` to set the response status to error. We decided to throw an exception when such a request fails. It is a trade-off between the API consistency and usability of the library. Usually, you only need the content of `RestResponse` instance to diagnose issues and most of the time the exception would tell you what's wrong. + +Below, you can find how different extensions deal with errors. Note that functions, which don't throw by default, will throw exceptions when `ThrowOnAnyError` is set to `true`. + +| Function | Throws on errors | +|:----------------------|:-----------------| +| `ExecuteAsync` | No | +| `ExecuteGetAsync` | No | +| `ExecuteGetAsync` | No | +| `ExecutePostAsync` | No | +| `ExecutePostAsync` | No | +| `ExecutePutAsync` | No | +| `ExecutePutAsync` | No | +| `GetAsync` | Yes | +| `GetAsync` | Yes | +| `PostAsync` | Yes | +| `PostAsync` | Yes | +| `PatchAsync` | Yes | +| `PatchAsync` | Yes | +| `DeleteAsync` | Yes | +| `DeleteAsync` | Yes | +| `OptionsAsync` | Yes | +| `OptionsAsync` | Yes | +| `HeadAsync` | Yes | +| `HeadAsync` | Yes | + +In addition, all the functions for JSON requests, like `GetJsonAsync` and `PostJsonAsync` throw an exception if the HTTP call fails. diff --git a/docs/versioned_docs/version-v112/advanced/interceptors.md b/docs/versioned_docs/version-v112/advanced/interceptors.md new file mode 100644 index 000000000..a3bf7a9bc --- /dev/null +++ b/docs/versioned_docs/version-v112/advanced/interceptors.md @@ -0,0 +1,84 @@ +--- +title: Interceptors +--- + +## Intercepting requests and responses + +Interceptors are a powerful feature of RestSharp that allows you to modify requests and responses before they are sent or received. You can use interceptors to add headers, modify the request body, or even cancel the request. You can also use interceptors to modify the response before it is returned to the caller. + +### Implementing an interceptor + +To implement an interceptor, you need to create a class that inherits the `Interceptor` base class. The base class implements all interceptor methods as virtual, so you can override them in your derived class. + +Methods that you can override are: +- `BeforeRequest(RestRequest request, CancellationToken cancellationToken)` +- `AfterRequest(RestResponse response, CancellationToken cancellationToken)` +- `BeforeHttpRequest(HttpRequestMessage requestMessage, CancellationToken cancellationToken)` +- `AfterHttpRequest(HttpResponseMessage responseMessage, CancellationToken cancellationToken)` +- `BeforeDeserialization(RestResponse response, CancellationToken cancellationToken)` + +All those functions must return a `ValueTask` instance. + +Here's an example of an interceptor that adds a header to a request: + +```csharp +// This interceptor adds a header to the request +// You'd not normally use this interceptor, as RestSharp already has a method +// to add headers to the request +class HeaderInterceptor(string headerName, string headerValue) : Interceptors.Interceptor { + public override ValueTask BeforeHttpRequest(HttpRequestMessage requestMessage, CancellationToken cancellationToken) { + requestMessage.Headers.Add(headerName, headerValue); + return ValueTask.CompletedTask; + } +} +``` + +Because interceptor functions return `ValueTask`, you can use `async` and `await` inside them. + +### Using an interceptor + +It's possible to add as many interceptors as you want, both to the client and to the request. The interceptors are executed in the order they were added. + +Adding interceptors to the client is done via the client options: + +```csharp +var options = new RestClientOptions("https://api.example.com") { + Interceptors = [new HeaderInterceptor("Authorization", token)] +}; +var client = new RestClient(options); +``` + +When you add an interceptor to the client, it will be executed for every request made by that client. + +You can also add an interceptor to a specific request: + +```csharp +var request = new RestRequest("resource") { + Interceptors = [new HeaderInterceptor("Authorization", token)] +}; +``` + +In this case, the interceptor will only be executed for that specific request. + +### Deprecation notice + +Interceptors aim to replace the existing request hooks available in RestSharp prior to version 111.0. Those hooks are marked with `Obsolete` attribute and will be removed in the future. If you are using those hooks, we recommend migrating to interceptors as soon as possible. + +To make the migration easier, RestSharp provides a class called `CompatibilityInterceptor`. It has properties for the hooks available in RestSharp 110.0 and earlier. You can use it to migrate your code to interceptors without changing the existing logic. + +For example, a code that uses `OnBeforeRequest` hook: + +```csharp +var request = new RestRequest("success"); +request.OnBeforeDeserialization += _ => throw new Exception(exceptionMessage); +``` + +Can be migrated to interceptors like this: + +```csharp +var request = new RestRequest("success") { + Interceptors = [new CompatibilityInterceptor { + OnBeforeDeserialization = _ => throw new Exception(exceptionMessage) + }] +}; +``` \ No newline at end of file diff --git a/docs/versioned_docs/version-v112/advanced/serialization.md b/docs/versioned_docs/version-v112/advanced/serialization.md new file mode 100644 index 000000000..b7092519a --- /dev/null +++ b/docs/versioned_docs/version-v112/advanced/serialization.md @@ -0,0 +1,148 @@ +# Serialization + +One of the most common reasons to choose RestSharp over plain `HttpClient` is its rich build-in serialization support. RestSharp allows adding complex objects as request body to be serialized when making a call to an API endpoint, and deserializing the response to a given .NET type. RestSharp supports JSON and XML serialization and deserialization by default. In addition, you can use a CSV serializer or write your own. + +In contrast to `System.Net.Http.Json` package that contains `HttpClient` extensions to make `GET` or `POST` calls using JSON, RestSharp support JSON responses for all HTTP methods, not just for `GET`. + +## Configuration + +:::tip +The default behavior of RestSharp is to swallow deserialization errors and return `null` in the `Data` +property of the response. Read more about it in the [Error Handling](error-handling.md). +::: + +You can tell RestSharp to use a custom serializer by using the `configureSerialization` constructor parameter: + +```csharp +var client = new RestClient( + options, + configureSerialization: s => s.UseSerializer(() => new CustomSerializer()); +); +``` + +All RestSharp serializers implement the `IRestSerializer` interface. Among other things, the interface requires implementing the `AcceptedContentTypes` property, which must return a collection of content types supported by the serializer. Being configured to use certain serializers, RestSharp populates the `Accept` header accordingly, so it doesn't need to be set manually. + +When making a call, RestSharp sets the request content type according to the request body type. For example, when you use `AddJsonBody`, the content type is set to `application/json`. Normally, you won't need to set the `Content-Type` header manually. If you need to set a custom content type for a JSON call, you can use the optional `contentType` argument of `AddJsonBody`, for example: + +```csharp +request.AddJsonBody(data, "text/json"); +``` + +## JSON + +The default JSON serializer uses `System.Text.Json`, which is a part of .NET since .NET 6. For earlier versions, it is added as a dependency. There are also a few serializers provided as additional packages. + +By default, RestSharp will use `JsonSerializerDefaults.Web` configuration. If necessary, you can specify your own options: + +```csharp +var client = new RestClient( + options, + configureSerialization: s => s.UseSystemTextJson(new JsonSerializerOptions {...}) +); +``` + +## XML + +The default XML serializer is `DotNetXmlSerializer`, which uses `System.Xml.Serialization` library from .NET. + +In previous versions of RestSharp, the default XML serializer was a custom RestSharp XML serializer. To make the code library size smaller, that serializer is now available as a separate package [`RestSharp.Serializers.Xml`](https://www.nuget.org/packages/RestSharp.Serializers.Xml). +You can add it back if necessary by installing the package and adding it to the client: + +```csharp +var client = new RestClient( + options, + configureSerialization: s => s.UseXmlSerializer() +); +``` + +As before, you can supply three optional arguments for a custom namespace, custom root element, and if you want to use `SerializeAs` and `DeserializeAs` attributed. + +## NewtonsoftJson (aka Json.Net) + +The `NewtonsoftJson` package is the most popular JSON serializer for .NET. It handles all possible scenarios and is very configurable. Such a flexibility comes with the cost of performance. If you need speed, keep the default JSON serializer. + +RestSharp support Json.Net serializer via a separate package [`RestSharp.Serializers.NewtonsoftJson`](https://www.nuget.org/packages/RestSharp.Serializers.NewtonsoftJson). + +:::warning +Please note that `RestSharp.Newtonsoft.Json` package is not provided by RestSharp, is marked as obsolete on NuGet, and no longer supported by its creator. +::: + +Use the extension method provided by the package to configure the client: + +```csharp +var client = new RestClient( + options, + configureSerialization: s => s.UseNewtonsoftJson() +); +``` + +The serializer configures some options by default: + +```csharp +JsonSerializerSettings DefaultSettings = new JsonSerializerSettings { + ContractResolver = new CamelCasePropertyNamesContractResolver(), + DefaultValueHandling = DefaultValueHandling.Include, + TypeNameHandling = TypeNameHandling.None, + NullValueHandling = NullValueHandling.Ignore, + Formatting = Formatting.None, + ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor +}; +``` + +If you need to use different settings, you can supply your instance of +`JsonSerializerSettings` as a parameter for the extension method. + +## CSV + +A separate package `RestSharp.Serializers.CsvHelper` provides a CSV serializer for RestSharp. It is based on the +`CsvHelper` library. + +Use the extension method provided by the package to configure the client: + +```csharp +var client = new RestClient( + options, + configureSerialization: s => s.UseCsvHelper() +); +``` + +You can also supply your instance of `CsvConfiguration` as a parameter for the extension method. + +```csharp +var client = new RestClient( + options, + configureSerialization: s => s.UseCsvHelper( + new CsvConfiguration(CultureInfo.InvariantCulture) {...} + ) +); +``` + +## Custom + +You can also implement your custom serializer. To support both serialization and +deserialization, you must implement the `IRestSerializer` interface. + +Here is an example of a custom serializer that uses `System.Text.Json`: + +```csharp +public class SimpleJsonSerializer : IRestSerializer { + public string? Serialize(object? obj) => obj == null ? null : JsonSerializer.Serialize(obj); + + public string? Serialize(Parameter bodyParameter) => Serialize(bodyParameter.Value); + + public T? Deserialize(RestResponse response) => JsonSerializer.Deserialize(response.Content!); + + public ContentType ContentType { get; set; } = ContentType.Json; + + public ISerializer Serializer => this; + public IDeserializer Deserializer => this; + public DataFormat DataFormat => DataFormat.Json; + public string[] AcceptedContentTypes => ContentType.JsonAccept; + public SupportsContentType SupportsContentType + => contentType => contentType.Value.EndsWith("json", StringComparison.InvariantCultureIgnoreCase); +} +``` + +The `SupportedContentTypes` function will be used to check if the serializer is able to deserialize the response based on the `Content-Type` response header. + +The `ContentType` property will be used when making a request so the server knows how to handle the payload. diff --git a/docs/versioned_docs/version-v112/changelog.md b/docs/versioned_docs/version-v112/changelog.md new file mode 100644 index 000000000..3a6d9513a --- /dev/null +++ b/docs/versioned_docs/version-v112/changelog.md @@ -0,0 +1,19 @@ +--- +title: What's new +description: List of changes for the current major version +sidebar_position: 1 +--- + +# Changelog + +For release notes of previous versions, please check the [Releases page](https://github.com/restsharp/RestSharp/releases) in RestSharp GitHub repository. + +Changes between major versions are documented in the documentation for each version on this website. + +# v112.0 + +* Security fix for [CVE-2024-45302](https://github.com/restsharp/RestSharp/security/advisories/GHSA-4rr6-2v9v-wcpc). Header values cannot contain `CRLF`. + +## v112.1 + +* Follow up on v112.0 security fix: remove `\t` from the list of forbidden characters in headers. \ No newline at end of file diff --git a/docs/versioned_docs/version-v112/intro.md b/docs/versioned_docs/version-v112/intro.md new file mode 100644 index 000000000..43fa8e2f0 --- /dev/null +++ b/docs/versioned_docs/version-v112/intro.md @@ -0,0 +1,112 @@ +--- +sidebar_position: 2 +title: Quick start +--- + +## Introduction + +:::warning +RestSharp v107+ changes the library API surface and its behaviour significantly. We advise looking at [migration](/migration) docs to understand how to migrate to the latest version of RestSharp. +::: + +The main purpose of RestSharp is to make synchronous and asynchronous calls to remote resources over HTTP. As the name suggests, the main audience of RestSharp are developers who use REST APIs. However, RestSharp can call any API over HTTP, as long as you have the resource URI and request parameters that you want to send comply with W3C HTTP standards. + +One of the main challenges of using HTTP APIs for .NET developers is to work with requests and responses of different kinds and translate them to complex C# types. RestSharp can take care of serializing the request body to JSON or XML and deserialize the response. It can also form a valid request URI based on different parameter kinds: path, query, form or body. + +## Getting Started + +Before you can use RestSharp in your application, you need to add the NuGet package. You can do it using your IDE or the command line: + +``` +dotnet add package RestSharp +``` + +### Basic Usage + +If you only have a small number of one-off API requests to perform, you can use RestSharp like this: + +```csharp +using RestSharp; +using RestSharp.Authenticators; + +var options = new RestClientOptions("https://api.twitter.com/1.1") { + Authenticator = new HttpBasicAuthenticator("username", "password") +}; +var client = new RestClient(options); +var request = new RestRequest("statuses/home_timeline.json"); +// The cancellation token comes from the caller. You can still make a call without it. +var response = await client.GetAsync(request, cancellationToken); +``` + +It will return a `RestResponse` back, which contains all the information returned from the remote server. +You have access to the headers, content, HTTP status and more. + +You can also use generic overloads like `Get` to automatically deserialize the response into a .NET class. + +For example: + +```csharp +using RestSharp; +using RestSharp.Authenticators; + +var options = new RestClientOptions("https://api.twitter.com/1.1") { + Authenticator = new HttpBasicAuthenticator("username", "password") +}; +var client = new RestClient(options); + +var request = new RestRequest("statuses/home_timeline.json"); + +// The cancellation token comes from the caller. You can still make a call without it. +var timeline = await client.GetAsync(request, cancellationToken); +``` + +Both snippets above use the `GetAsync` extension, which is a wrapper about `ExecuteGetAsync`, which, in turn, is a wrapper around `ExecuteAsync`. +All `ExecuteAsync` overloads and return the `RestResponse` or `RestResponse`. + +The most important difference is that async methods named after HTTP methods (like `GetAsync` or `PostAsync`) return `Task` instead of `Task>`. It means that you won't get an error response if the request fails as those methods throw an exception for unsuccessful HTTP calls. For keeping the API consistent, non-generic functions like `GetAsync` or `PostAsync` also throw an exception if the request fails, although they return the `Task`. + +Read [here](advanced/error-handling.md) about how RestSharp handles exceptions. + +RestSharp also offers simple ways to call APIs that accept and return JSON payloads. You can use the `GetJsonAsync` and `PostJsonAsync` extension methods, which will automatically serialize the request body to JSON and deserialize the response to the specified type. + +```csharp +var client = new RestClient(options); +var timeline = await client.GetJsonAsync("statuses/home_timeline.json", cancellationToken); +``` + +Read [here](usage/execute.md#json-requests) about making JSON calls without preparing a request object. + +### Content type + +RestSharp supports sending XML or JSON body as part of the request. To add a body to the request, simply call `AddJsonBody` or `AddXmlBody` method of the `RestRequest` object. + +There is no need to set the `Content-Type` or add the `DataFormat` parameter to the request when using those methods, RestSharp will do it for you. + +RestSharp will also handle both XML and JSON responses and perform all necessary deserialization tasks, depending on the server response type. Therefore, you only need to add the `Accept` header if you want to deserialize the response manually. + +For example, only you'd only need these lines to make a request with JSON body: + +```csharp +var request = new RestRequest("address/update").AddJsonBody(updatedAddress); +var response = await client.PostAsync(request); +``` + +It's also possible to make the same call using `PostAsync` shorter syntax: + +```csharp +var response = await client.PostJsonAsync( + "address/update", updatedAddress, cancellationToken +); +``` + +Read more about serialization and deserialization [here](advanced/serialization.md). + +### Response + +When you use `ExecuteAsync`, you get an instance of `RestResponse` back. The response object has the `Content` property, which contains the response as string. You can find other useful properties there, like `StatusCode`, `ContentType` and so on. If the request wasn't successful, you'd get a response back with `IsSuccessful` property set to `false` and the error explained in the `ErrorException` and `ErrorMessage` properties. + +When using typed `ExecuteAsync`, you get an instance of `RestResponse` back, which is identical to `RestResponse` but also contains the `T Data` property with the deserialized response. + +None of `ExecuteAsync` overloads throw if the remote server returns an error. You can inspect the response and find the status code, error message, and, potentially, an exception. + +Extensions like `GetAsync` will not return the whole `RestResponse` but just a deserialized response. These extensions will throw an exception if the remote server returns an error. The exception details contain the status code returned by the server. diff --git a/docs/versioned_docs/version-v112/usage/_category_.json b/docs/versioned_docs/version-v112/usage/_category_.json new file mode 100644 index 000000000..bd2045cd6 --- /dev/null +++ b/docs/versioned_docs/version-v112/usage/_category_.json @@ -0,0 +1,7 @@ +{ + "label": "Using RestSharp", + "position": 3, + "link": { + "type": "generated-index" + } +} diff --git a/docs/versioned_docs/version-v112/usage/basics.md b/docs/versioned_docs/version-v112/usage/basics.md new file mode 100644 index 000000000..6bbb5594a --- /dev/null +++ b/docs/versioned_docs/version-v112/usage/basics.md @@ -0,0 +1,23 @@ +--- +sidebar_position: 2 +--- + +# RestSharp basics + +This page describes some of the essential properties and features of RestSharp. + +## What RestSharp does + +Essentially, RestSharp is a wrapper around `HttpClient` that allows you to do the following: +- Add default parameters of any kind (not just headers) to the client, once +- Add parameters of any kind to each request (query, URL segment, form, attachment, serialized body, header) in a straightforward way +- Serialize the payload to JSON or XML if necessary +- Set the correct content headers (content type, disposition, length, etc.) +- Handle the remote endpoint response +- Deserialize the response from JSON or XML if necessary + +## API client + +The best way to call an external HTTP API is to create a typed client, which encapsulates RestSharp calls and doesn't expose the `RestClient` instance in public. + +You can find an example of a Twitter API client on the [Example](example.md) page. diff --git a/docs/versioned_docs/version-v112/usage/client.md b/docs/versioned_docs/version-v112/usage/client.md new file mode 100644 index 000000000..e84b955f4 --- /dev/null +++ b/docs/versioned_docs/version-v112/usage/client.md @@ -0,0 +1,115 @@ +--- +sidebar_position: 3 +title: Creating the client +--- + +## Constructors + +A RestSharp client can be instantiated by one of its constructors. Two most commonly used constructors are: + +#### Only specify the base URL + +You can create an instance of `RestClient` with only a single parameter: the base URL. Even that isn't required as base URL can be left empty. In that case, you'd need to specify the absolute path for each call. When the base URL is set, you can use both relative and absolute path. + +```csharp +// Creates a client with default options to call a given base URL +var client = new RestClient("https://localhost:5000"); +``` + +#### Provide client options + +The most common way to create a client is to use the constructor with options. The options object has the type of `RestClientOptions`. +Here's an example of how to create a client using the same base path as in the previous sample, but with a couple additional settings: + +```csharp +// Creates a client using the options object +var options = new RestClientOptions("https://localhost:5000") { + MaxTimeout = 1000 +}; +var client = new RestClient(options); +``` + +#### Advanced configuration + +RestSharp can be configured with more tweaks, including default request options, how it should handle responses, how serialization works, etc. You can also provide your own instance of `HttpClient` or `HttpMessageHandler`. + +Read more about the advanced configuration of RestSharp on a [dedicated page](../advanced/configuration.md). + +## Simple factory + +Another way to create the client instance is to use a simple client factory. The factory will use the `BaseUrl` property of the client options to cache `HttpClient` instances. Every distinct base URL will get its own `HttpClient` instance. Other options don't affect the caching. Therefore, if you use different options for the same base URL, you'll get the same `HttpClient` instance, which will not be configured with the new options. Options that aren't applied _after_ the first client instance is created are: + +* `Credentials` +* `UseDefaultCredentials` +* `AutomaticDecompression` +* `PreAuthenticate` +* `FollowRedirects` +* `RemoteCertificateValidationCallback` +* `ClientCertificates` +* `MaxRedirects` +* `MaxTimeout` +* `UserAgent` +* `Expect100Continue` + +Constructor parameters to configure the `HttpMessageHandler` and default `HttpClient` headers configuration are also ignored for the cached instance as the factory only configures the handler once. + +You need to set the `useClientFactory` parameter to `true` in the `RestClient` constructor to enable the factory. + +```csharp +var options = new RestClientOptions("https://api.twitter.com/2"); +var client = new RestClient(options, useClientFactory: true); +``` + +## Reusing HttpClient + +RestSharp uses `HttpClient` internally to make HTTP requests. It's possible to reuse the same `HttpClient` instance for multiple `RestClient` instances. This is useful when you want to share the same connection pool between multiple `RestClient` instances. + +One way of doing it is to use `RestClient` constructors that accept an instance of `HttpClient` or `HttpMessageHandler` as an argument. Note that in that case not all the options provided via `RestClientOptions` will be used. Here is the list of options that will work: + +- `BaseAddress` is be used to set the base address of the `HttpClient` instance if base address is not set there already. +- `MaxTimeout` is used to cancel the call using the cancellation token source, so +- `UserAgent` will be added to the `RestClient.DefaultParameters` list as a HTTP header. This will be added to each request made by the `RestClient`, and the `HttpClient` instance will not be modified. This is to allow the `HttpClient` instance to be reused for scenarios where different `User-Agent` headers are required. +- `Expect100Continue` + +Another option is to use a simple HTTP client factory as described [above](#simple-factory). + +## Blazor support + +Inside a Blazor webassembly app, you can make requests to external API endpoints. Microsoft examples show how to do it with `HttpClient`, and it's also possible to use RestSharp for the same purpose. + +You need to remember that webassembly has some platform-specific limitations. Therefore, you won't be able to instantiate `RestClient` using all of its constructors. In fact, you can only use `RestClient` constructors that accept `HttpClient` or `HttpMessageHandler` as an argument. If you use the default parameterless constructor, it will call the option-based constructor with default options. The options-based constructor will attempt to create an `HttpMessageHandler` instance using the options provided, and it will fail with Blazor, as some of those options throw thw "Unsupported platform" exception. + +Here is an example how to register the `RestClient` instance globally as a singleton: + +```csharp +builder.Services.AddSingleton(new RestClient(new HttpClient())); +``` + +Then, on a page you can inject the instance: + +```html +@page "/fetchdata" +@using RestSharp +@inject RestClient _restClient +``` + +And then use it: + +```csharp +@code { + private WeatherForecast[]? forecasts; + + protected override async Task OnInitializedAsync() { + forecasts = await _restClient.GetJsonAsync("http://localhost:5104/weather"); + } + + public class WeatherForecast { + public DateTime Date { get; set; } + public int TemperatureC { get; set; } + public string? Summary { get; set; } + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + } +} +``` + +In this case, the call will be made to a WebAPI server hosted at `http://localhost:5104/weather`. Remember that if the WebAPI server is not hosting the webassembly itself, it needs to have a CORS policy configured to allow the webassembly origin to access the API endpoint from the browser. diff --git a/docs/versioned_docs/version-v112/usage/example.md b/docs/versioned_docs/version-v112/usage/example.md new file mode 100644 index 000000000..6182d8ff3 --- /dev/null +++ b/docs/versioned_docs/version-v112/usage/example.md @@ -0,0 +1,152 @@ +--- +sidebar_position: 1 +--- + +# Example + +RestSharp works best as the foundation for a proxy class for your API. Each API would most probably require different settings for `RestClient`. Hence, a dedicated API class (and its interface) gives you sound isolation between different `RestClient` instances and make them testable. + +For example, let's look at a simple Twitter API v2 client, which uses OAuth2 machine-to-machine authentication. For it to work, you would need to have access to the Twitter Developers portal, a project, and an approved application inside the project with OAuth2 enabled. + +## Client model + +Before implementing an API client, we need to have a model for it. The model includes an abstraction for the client, which has functions for the API calls we are interested to implement. In addition, the client model would include the necessary request and response models. Usually those are simple classes or records without logic, which are often referred to as DTOs (data transfer objects). + +This example starts with a single function that retrieves one Twitter user. Lets being by defining the API client interface: + +```csharp +public interface ITwitterClient { + Task GetUser(string user); +} +``` + +As the function returns a `TwitterUser` instance, we need to define it as a model: + +```csharp +public record TwitterUser(string Id, string Name, string Username); +``` + +## Client implementation + +When that is done, we can implement the interface and add all the necessary code blocks to get a working API client. + +The client class needs the following: +- A constructor for passing API credentials +- A wrapped `RestClient` instance with the Twitter API base URI pre-configured +- An authenticator to support authorizing the client using Twitter OAuth2 authentication +- The actual function to get the user (to implement the `ITwitterClient` interface) + +Creating an authenticator is described [below](#authenticator). + +Here's how the client implementation could look like: + +```csharp +public class TwitterClient : ITwitterClient, IDisposable { + readonly RestClient _client; + + public TwitterClient(string apiKey, string apiKeySecret) { + var options = new RestClientOptions("https://api.twitter.com/2"); + _client = new RestClient(options); + } + + public async Task GetUser(string user) { + var response = await _client.GetAsync>( + "users/by/username/{user}", + new { user } + ); + return response!.Data; + } + + record TwitterSingleObject(T Data); + + public void Dispose() { + _client?.Dispose(); + GC.SuppressFinalize(this); + } +} +``` + +It is also possible to use ASP.NET Core Options for configuring the client, instead of passing the credentials as strings. For example, we can add a class for Twitter client options, and use it in a constructor: + +```csharp +public class TwitterClientOptions(string ApiKey, string ApiSecret); + +public TwitterClient(IOptions options) { + var opt = new RestClientOptions("https://api.twitter.com/2"); + _client = new RestClient(options); +} +``` + +Then, you can register and configure the client using ASP.NET Core dependency injection container. + +Right now, the client won't really work as Twitter API requires authentication. It's covered in the next section. + +## Authenticator + +Before we can call the API itself, we need to get a bearer token. Twitter exposes an endpoint `https://api.twitter.com/oauth2/token`. As it follows the OAuth2 conventions, the code can be used to create an authenticator for some other vendors. + +First, we need a model for deserializing the token endpoint response. OAuth2 uses snake case for property naming, so we need to decorate model properties with `JsonPropertyName` attribute: + +```csharp +record TokenResponse { + [JsonPropertyName("token_type")] + public string TokenType { get; init; } + [JsonPropertyName("access_token")] + public string AccessToken { get; init; } +} +``` + +Next, we create the authenticator itself. It needs the API key and API key secret to call the token endpoint using basic HTTP authentication. In addition, we can extend the list of parameters with the base URL to convert it to a more generic OAuth2 authenticator. + +The easiest way to create an authenticator is to inherit from the `AuthenticatorBase` base class: + +```csharp +public class TwitterAuthenticator : AuthenticatorBase { + readonly string _baseUrl; + readonly string _clientId; + readonly string _clientSecret; + + public TwitterAuthenticator(string baseUrl, string clientId, string clientSecret) : base("") { + _baseUrl = baseUrl; + _clientId = clientId; + _clientSecret = clientSecret; + } + + protected override async ValueTask GetAuthenticationParameter(string accessToken) { + Token = string.IsNullOrEmpty(Token) ? await GetToken() : Token; + return new HeaderParameter(KnownHeaders.Authorization, Token); + } +} +``` + +During the first call made by the client using the authenticator, it will find out that the `Token` property is empty. It will then call the `GetToken` function to get the token once and reuse the token going forward. + +Now, we need to implement the `GetToken` function in the class: + +```csharp +async Task GetToken() { + var options = new RestClientOptions(_baseUrl){ + Authenticator = new HttpBasicAuthenticator(_clientId, _clientSecret), + }; + using var client = new RestClient(options); + + var request = new RestRequest("oauth2/token") + .AddParameter("grant_type", "client_credentials"); + var response = await client.PostAsync(request); + return $"{response!.TokenType} {response!.AccessToken}"; +} +``` + +As we need to make a call to the token endpoint, we need our own short-lived instance of `RestClient`. Unlike the actual Twitter client, it will use the `HttpBasicAuthenticator` to send the API key and secret as the username and password. The client then gets disposed as we only use it once. + +Here we add a POST parameter `grant_type` with `client_credentials` as its value. At the moment, it's the only supported value. + +The POST request will use the `application/x-www-form-urlencoded` content type by default. + +::: note +Sample code provided on this page is a production code. For example, the authenticator might produce undesired side effect when multiple requests are made at the same time when the token hasn't been obtained yet. It can be solved rather than simply using semaphores or synchronized invocation. +::: + +## Final words + +This page demonstrates how an API client can be implemented as a typed, configurable client with its own interface. Usage of the client in applications is not covered here as different application types and target frameworks have their own idiomatic ways to use HTTP clients. \ No newline at end of file diff --git a/docs/versioned_docs/version-v112/usage/execute.md b/docs/versioned_docs/version-v112/usage/execute.md new file mode 100644 index 000000000..36343f793 --- /dev/null +++ b/docs/versioned_docs/version-v112/usage/execute.md @@ -0,0 +1,172 @@ +--- +sidebar_position: 5 +title: Making calls +--- + +## Executing requests + +Once you've added all the parameters to your `RestRequest`, you are ready to make a request. + +`RestClient` has a single function for this: + +```csharp +public async Task ExecuteAsync( + RestRequest request, + CancellationToken cancellationToken = default +) +``` + +You can also avoid setting the request method upfront and use one of the overloads: + +```csharp +Task ExecuteGetAsync(RestRequest request, CancellationToken cancellationToken) +Task ExecutePostAsync(RestRequest request, CancellationToken cancellationToken) +Task ExecutePutAsync(RestRequest request, CancellationToken cancellationToken) +Task ExecuteDeleteAsync(RestRequest request, CancellationToken cancellationToken) +Task ExecuteHeadAsync(RestRequest request, CancellationToken cancellationToken) +Task ExecuteOptionsAsync(RestRequest request, CancellationToken cancellationToken) +``` + +When using any of those methods, you will get the response content as string in `response.Content`. + +RestSharp can deserialize the response for you. To use that feature, use one of the generic overloads: + +```csharp +Task> ExecuteAsync(RestRequest request, CancellationToken cancellationToken) +Task> ExecuteGetAsync(RestRequest request, CancellationToken cancellationToken) +Task> ExecutePostAsync(RestRequest request, CancellationToken cancellationToken) +Task> ExecutePutAsync(RestRequest request, CancellationToken cancellationToken) +Task> ExecuteDeleteAsync(RestRequest request, CancellationToken cancellationToken) +Task> ExecuteHeadAsync(RestRequest request, CancellationToken cancellationToken) +Task> ExecuteOptionsAsync(RestRequest request, CancellationToken cancellationToken) +``` + +:::note Beware of errors +All the overloads with names starting with `Execute` don't throw an exception if the server returns an error. Read more about it [here](../advanced/error-handling.md). +It allows you to inspect responses and handle remote server errors gracefully. Overloads without `Execute` prefix throw exceptions in case of any error, so you'd need to ensure to handle exceptions properly. +::: + +If you just need a deserialized response, you can use one of the extensions: + +```csharp +Task GetAsync(RestRequest request, CancellationToken cancellationToken) +Task PostAsync(RestRequest request, CancellationToken cancellationToken) +Task PutAsync(RestRequest request, CancellationToken cancellationToken) +Task PatchAsync(RestRequest request, CancellationToken cancellationToken) +Task DeleteAsync(RestRequest request, CancellationToken cancellationToken) +Task HeadAsync(RestRequest request, CancellationToken cancellationToken) +Task OptionsAsync(RestRequest request, CancellationToken cancellationToken) +``` + +Those extensions will throw an exception if the server returns an error, as there's no other way to float the error back to the caller. + +The `IRestClient` interface also has extensions for making requests without deserialization, which throw an exception if the server returns an error even if the client is configured to not throw exceptions. + +```csharp +Task GetAsync(RestRequest request, CancellationToken cancellationToken) +Task PostAsync(RestRequest request, CancellationToken cancellationToken) +Task PutAsync(RestRequest request, CancellationToken cancellationToken) +Task PatchAsync(RestRequest request, CancellationToken cancellationToken) +Task DeleteAsync(RestRequest request, CancellationToken cancellationToken) +Task HeadAsync(RestRequest request, CancellationToken cancellationToken) +Task OptionsAsync(RestRequest request, CancellationToken cancellationToken) +``` + +### Sync calls + +The preferred way for making requests is to execute them asynchronously as HTTP calls are IO-bound operations. +If you are unable to make async calls, all the functions about have sync overloads, which have the same names without `Async` suffix. +For example, for making a sync `GET` call you can use `ExecuteGet(request)` or `Get`, etc. + +## Requests without body + +Some HTTP methods don't suppose to be used with request body. For those methods, RestSharp supports making simplified calls without using `RestRequest`. All you need is to provide the resource path as a string. + +For example, you can make a `DELETE` call like this: + +```csharp +var response = await client.ExecuteDeleteAsync($"order/delete/{orderId}", cancellationToken); +``` + +Similarly, you can make `GET` calls with or without deserialization of the response using `ExecuteGetAsync(resource)`, `GetAsync(resource)`, `ExecuteGetAsync(resource)`, and `GetAsync(resource)` (see below). + +## JSON requests + +RestSharp provides an easier API for making calls to endpoints that accept and return JSON. + +### GET calls + +To make a simple `GET` call and get a deserialized JSON response with a pre-formed resource string, use this: + +```csharp +var response = await client.GetAsync("endpoint?foo=bar", cancellationToken); +``` + +:::note +In v111, `GetJsonAsync` is renamed to `GetAsync`. +::: + +You can also use a more advanced extension that uses an object to compose the resource string: + +```csharp +var client = new RestClient("https://example.org"); +var args = new { + id = "123", + foo = "bar" +}; +// Will make a call to https://example.org/endpoint/123?foo=bar +var response = await client.GetAsync("endpoint/{id}", args, cancellationToken); +``` + +It will search for the URL segment parameters matching any of the object properties and replace them with values. All the other properties will be used as query parameters. + +One note about `GetAsync` is that it will deserialize the response with any supported content type, not only JSON. + +### POST calls + +Similar things are available for `POST` requests. + +```csharp +var request = new CreateOrder("123", "foo", 10100); +// Will post the request object as JSON to "orders" and returns a +// JSON response deserialized to OrderCreated +var result = client.PostJsonAsync("orders", request, cancellationToken); +``` + +```csharp +var request = new CreateOrder("123", "foo", 10100); +// Will post the request object as JSON to "orders" and returns a +// status code, not expecting any response body +var statusCode = client.PostJsonAsync("orders", request, cancellationToken); +``` + +The same two extensions also exist for `PUT` requests (`PutJsonAsync`); + +## Downloading binary data + +There are two functions that allow you to download binary data from the remote API. + +First, there's `DownloadDataAsync`, which returns `Task`. This function allows you to open a stream reader and asynchronously stream large responses to memory or disk. + +## JSON streaming + +For HTTP API endpoints that stream the response data (like [Twitter search stream](https://developer.twitter.com/en/docs/twitter-api/tweets/filtered-stream/api-reference/get-tweets-search-stream)) you can use RestSharp with `StreamJsonAsync`, which returns an `IAsyncEnumerable`: + +```csharp +public async IAsyncEnumerable SearchStream( + [EnumeratorCancellation] CancellationToken cancellationToken = default +) { + var response = _client.StreamJsonAsync>( + "tweets/search/stream", cancellationToken + ); + + await foreach (var item in response.WithCancellation(cancellationToken)) { + yield return item.Data; + } +} +``` + +The main limitation of this function is that it expects each JSON object to be returned as a single line. It is unable to parse the response by combining multiple lines into a JSON string. + diff --git a/docs/versioned_docs/version-v112/usage/request.md b/docs/versioned_docs/version-v112/usage/request.md new file mode 100644 index 000000000..374ee14dd --- /dev/null +++ b/docs/versioned_docs/version-v112/usage/request.md @@ -0,0 +1,350 @@ +--- +sidebar_position: 4 +title: Preparing requests +--- + +## Create a request + +Before making a request using `RestClient`, you need to create a request instance: + +```csharp +var request = new RestRequest(resource); // resource is the sub-path of the client base path +``` + +The default request type is `GET` and you can override it by setting the `Method` property. You can also set the method using the constructor overload: + +```csharp +var request = new RestRequest(resource, Method.Post); +``` + +After you've created a `RestRequest`, you can add parameters to it. Below, you can find all the parameter types supported by RestSharp. + +## Request headers + +Adds the header parameter as an HTTP header that is sent along with the request. The header name is the parameter's name and the header value is the value. + +You can use one of the following request methods to add a header parameter: + +```csharp +AddHeader(string name, string value); +AddHeader(string name, T value); // value will be converted to string +AddOrUpdateHeader(string name, string value); // replaces the header if it already exists +``` + +For example: + +```csharp +var request = new RestRequest("/path").AddHeader("X-Key", someKey); +``` + +You can also add header parameters to the client, and they will be added to every request made by the client. This is useful for adding authentication headers, for example. + +```csharp +client.AddDefaultHeader(string name, string value); +``` + +:::warning Avoid setting Content-Type header +RestSharp will use the correct content type by default. Avoid adding the `Content-Type` header manually to your requests unless you are absolutely sure it is required. You can add a custom content type to the [body parameter](#request-body) itself. +::: + +## Get or Post parameters + +The default RestSharp parameter type is `GetOrPostParameter`. You can add `GetOrPost` parameter to the request using the `AddParameter` function: + +```csharp +request + .AddParameter("name1", "value1") + .AddParameter("name2", "value2"); +``` + +`GetOrPost` behaves differently based on the HTTP method. If you execute a `GET` call, RestSharp will append the parameters to the URL in the form `url?name1=value1&name2=value2`. + +On a `POST` or `PUT` requests, it depends on whether you have files attached to a request. +If not, the parameters will be sent as the body of the request in the form `name1=value1&name2=value2`. Also, the request will be sent as `application/x-www-form-urlencoded`. + +In both cases, name and value will automatically be URL-encoded, unless specified otherwise: + +```csharp +request.AddParameter("name", "Væ üé", false); // don't encode the value +``` + +If you have files, RestSharp will send a `multipart/form-data` request. Your parameters will be part of this request in the form: + +``` +Content-Type: text/plain; charset=utf-8 +Content-Disposition: form-data; name="parameterName" + +ParameterValue +``` + +Sometimes, you need to override the default content type for the parameter when making a multipart form call. It's possible to do by setting the `ContentType` property of the parameter object. As an example, the code below will create a POST parameter with JSON value, and set the appropriate content type: + +```csharp +var parameter = new GetOrPostParameter("someJson", "{\"attributeFormat\":\"pdf\"}") { + ContentType = "application/json" +}; +request.AddParameter(parameter); +``` + +When the request is set to use multipart content, the parameter will be sent as part of the request with the specified content type: + +``` +Content-Type: application/json; charset=utf-8 +Content-Disposition: form-data; name="someJson" + +{"attributeFormat":"pdf"} +``` + +You can also add `GetOrPost` parameter as a default parameter to the client. This will add the parameter to every request made by the client. + +```csharp +client.AddDefaultParameter("foo", "bar"); +``` + +It will work the same way as request parameters, except that it will be added to every request. + +## Query string + +`QueryString` works like `GetOrPost`, except that it always appends the parameters to the url in the form `url?name1=value1&name2=value2`, regardless of the request method. + +Example: + +```csharp +var client = new RestClient("https://search.me"); +var request = new RestRequest("search") + .AddParameter("foo", "bar"); +var response = await client.GetAsync(request); +``` + +It will send a `GET` request to `https://search.me/search?foo=bar`. + +For `POST`-style requests you need to add the query string parameter explicitly: + +```csharp +request.AddQueryParameter("foo", "bar"); +``` + +In some cases, you might need to prevent RestSharp from encoding the query string parameter. +To do so, set the `encode` argument to `false` when adding the parameter: + +```csharp +request.AddQueryParameter("foo", "bar/fox", false); +``` + +You can also add a query string parameter as a default parameter to the client. This will add the parameter to every request made by the client. + +```csharp +client.AddDefaultQueryParameter("foo", "bar"); +``` + +The line above will result in all the requests made by that client instance to have `foo=bar` in the query string for all the requests made by that client. + +## Using AddObject + +You can avoid calling `AddParameter` multiple times if you collect all the parameters in an object, and then use `AddObject`. +For example, this code: + +```csharp +var params = new { + status = 1, + priority = "high", + ids = new [] { "123", "456" } +}; +request.AddObject(params); +``` + +is equivalent to: + +```csharp +request.AddParameter("status", 1); +request.AddParameter("priority", "high"); +request.AddParameter("ids", "123,456"); +``` + +Remember that `AddObject` only works if your properties have primitive types. It also works with collections of primitive types as shown above. + +If you need to override the property name or format, you can do it using the `RequestProperty` attribute. For example: + +```csharp +public class RequestModel { + // override the name and the format + [RequestProperty(Name = "from_date", Format = "d")] + public DateTime FromDate { get; set; } +} + +// add it to the request +request.AddObject(new RequestModel { FromDate = DateTime.Now }); +``` + +In this case, the request will get a GET or POST parameter named `from_date` and its value would be the current date in short date format. + +## Using AddObjectStatic + +Request function `AddObjectStatic(...)` allows using pre-compiled expressions for getting property values. Compared to `AddObject` that uses reflections for each call, `AddObjectStatic` caches functions to retrieve properties from an object of type `T`, so it works much faster. + +You can instruct `AddObjectStatic` to use custom parameter names and formats, as well as supply the list of properties than need to be used as parameters. The last option could be useful if the type `T` has properties that don't need to be sent with HTTP call. + +To use custom parameter name or format, use the `RequestProperty` attribute. For example: + +```csharp +class TestObject { + [RequestProperty(Name = "some_data")] + public string SomeData { get; set; } + + [RequestProperty(Format = "d")] + public DateTime SomeDate { get; set; } + + [RequestProperty(Name = "dates", Format = "d")] + public DateTime[] DatesArray { get; set; } + + public int Plain { get; set; } + public DateTime[] PlainArray { get; set; } +} +``` + +## URL segment parameter + +Unlike `GetOrPost`, URL segment parameter replaces placeholder values in the request URL: + +```csharp +var request = new RestRequest("health/{entity}/status") + .AddUrlSegment("entity", "s2"); +``` + +When the request executes, RestSharp will try to match any `{placeholder}` with a parameter of that name (without the `{}`) and replace it with the value. So the above code results in `health/s2/status` being the URL. + +You can also add `UrlSegment` parameter as a default parameter to the client. This will add the parameter to every request made by the client. + +```csharp +client.AddDefaultUrlSegment("foo", "bar"); +``` + +## Cookies + +You can add cookies to a request using the `AddCookie` method: + +```csharp +request.AddCookie("foo", "bar"); +``` + +RestSharp will add cookies from the request as cookie headers and then extract the matching cookies from the response. You can observe and extract response cookies using the `RestResponse.Cookies` properties, which has the `CookieCollection` type. + +However, the usage of a default URL segment parameter is questionable as you can just include the parameter value to the base URL of the client. There is, however, a `CookieContainer` instance on the request level. You can either assign the pre-populated container to `request.CookieContainer`, or let the container be created by the request when you call `AddCookie`. Still, the container is only used to extract all the cookies from it and create cookie headers for the request instead of using the container directly. It's because the cookie container is normally configured on the `HttpClientHandler` level and cookies are shared between requests made by the same client. In most of the cases this behaviour can be harmful. + +If your use case requires sharing cookies between requests made by the client instance, you can use the client-level `CookieContainer`, which you must provide as the options' property. You can add cookies to the container using the container API. No response cookies, however, would be auto-added to the container, but you can do it in code by getting cookies from the `Cookes` property of the response and adding them to the client-level container available via `IRestClient.Options.CookieContainer` property. + +## Request Body + +RestSharp supports multiple ways to add a request body: +- `AddJsonBody` for JSON payloads +- `AddXmlBody` for XML payloads +- `AddStringBody` for pre-serialized payloads + +We recommend using `AddJsonBody` or `AddXmlBody` methods instead of `AddParameter` with type `BodyParameter`. Those methods will set the proper request type and do the serialization work for you. + +When you make a `POST`, `PUT` or `PATCH` request and added `GetOrPost` [parameters](#get-or-post-parameters), RestSharp will send them as a URL-encoded form request body by default. When a request also has files, it will send a `multipart/form-data` request. You can also instruct RestSharp to send the body as `multipart/form-data` by setting the `AlwaysMultipartFormData` property to `true`. + +You can specify a custom body content type if necessary. The `contentType` argument is available in all the overloads that add a request body. + +It is not possible to add client-level default body parameters. + +### String body + +If you have a pre-serialized payload like a JSON string, you can use `AddStringBody` to add it as a body parameter. You need to specify the content type, so the remote endpoint knows what to do with the request body. For example: + +```csharp +const json = "{ data: { foo: \"bar\" } }"; +request.AddStringBody(json, ContentType.Json); +``` + +### JSON body + +When you call `AddJsonBody`, it does the following for you: + +- Instructs the RestClient to serialize the object parameter as JSON when making a request +- Sets the content type to `application/json` +- Sets the internal data type of the request body to `DataType.Json` + +Here is the example: + +```csharp +var param = new MyClass { IntData = 1, StringData = "test123" }; +request.AddJsonBody(param); +``` + +It is possible to override the default content type by supplying the `contentType` argument. For example: + +```csharp +request.AddJsonBody(param, "text/x-json"); +``` + +If you use a pre-serialized string with `AddJsonBody`, it will be sent as-is. The `AddJsonBody` will detect if the parameter is a string and will add it as a string body with JSON content type. +Essentially, it means that top-level strings won't be serialized as JSON when you use `AddJsonBody`. To overcome this issue, you can use an overload of `AddJsonBody`, which allows you to tell RestSharp to serialize the string as JSON: + +```csharp +const string payload = @" +""requestBody"": { + ""content"": { + ""application/json"": { + ""schema"": { + ""type"": ""string"" + } + } + } +},"; +request.AddJsonBody(payload, forceSerialize: true); // the string will be serialized +request.AddJsonBody(payload); // the string will NOT be serialized and will be sent as-is +``` + +### XML body + +When you call `AddXmlBody`, it does the following for you: + +- Instructs the RestClient to serialize the object parameter as XML when making a request +- Sets the content type to `application/xml` +- Sets the internal data type of the request body to `DataType.Xml` + +:::warning +Do not send XML string to `AddXmlBody`; it won't work! +::: + +## Uploading files + +To add a file to the request you can use the `RestRequest` function called `AddFile`. The main function accepts the `FileParameter` argument: + +```csharp +request.AddFile(fileParameter); +``` + +You can instantiate the file parameter using `FileParameter.Create` that accepts a bytes array, or `FileParameter.FromFile`, which will load the file from disk. + +There are also extension functions that wrap the creation of `FileParameter` inside: + +```csharp +// Adds a file from disk +AddFile(parameterName, filePath, contentType); + +// Adds an array of bytes +AddFile(parameterName, bytes, fileName, contentType); + +// Adds a stream returned by the getFile function +AddFile(parameterName, getFile, fileName, contentType); +``` + +Remember that `AddFile` will set all the necessary headers, so please don't try to set content headers manually. + +You can also provide file upload options to the `AddFile` call. The options are: +- `DisableFilenameEncoding` (default `false`): if set to `true`, RestSharp will not encode the file name in the `Content-Disposition` header +- `DisableFilenameStar` (default `true`): if set to `true`, RestSharp will not add the `filename*` parameter to the `Content-Disposition` header + +Example of using the options: + +```csharp +var options = new FileParameterOptions { + DisableFilenameEncoding = true, + DisableFilenameStar = false +}; +request.AddFile("file", filePath, options: options); +``` + +The options specified in the snippet above usually help when you upload files with non-ASCII characters in their names. diff --git a/docs/versioned_docs/version-v112/usage/response.md b/docs/versioned_docs/version-v112/usage/response.md new file mode 100644 index 000000000..dbaf302b4 --- /dev/null +++ b/docs/versioned_docs/version-v112/usage/response.md @@ -0,0 +1,36 @@ +--- +sidebar_position: 6 +title: Handling responses +--- + +All `Execute{Method}Async` functions return an instance of `RestResponse`. Similarly, `Execute{Method}Async` return a generic instance of `RestResponse` where `T` is the response object type. + +Response object contains the following properties: + +| Property | Type | Description | +|--------------------------|-----------------------------------------------------|------------------------------------------------------------------------------------------| +| `Request` | `RestRequest` | Request instance that was used to get the response. | +| `ContentType` | `string?` | Response content type. `Null` if response has no content. | +| `ContentLength` | `long?` | Response content length. `Null` if response has no content. | +| `ContentEncoding` | `ICollection` | Content encoding collection. Empty if response has no content. | +| `Content` | `string?` | Response content as string. `Null` if response has no content. | +| `IsSuccessfulStatusCode` | `bool` | Indicates if response was successful, so no errors were reported by the server. | +| `ResponseStatus` | `None`, `Completed`, `Error`, `TimedOut`, `Aborted` | Response completion status. Note that completed responses might still return errors. | +| `IsSuccessful` | `bool` | `True` when `IsSuccessfulStatusCode` is `true` and `ResponseStatus` is `Completed`. | +| `StatusDescription` | `string?` | Response status description, if available. | +| `RawBytes` | `byte[]?` | Response content as byte array. `Null` if response has no content. | +| `ResponseUri` | `Uri?` | URI of the response, which might be different from request URI in case of redirects. | +| `Server` | `string?` | Server header value of the response. | +| `Cookies` | `CookieCollection?` | Collection of cookies received with the response, if any. | +| `Headers` | Collection of `HeaderParameter` | Response headers. | +| `ContentHeaders` | Collection of `HeaderParameter` | Response content headers. | +| `ErrorMessage` | `string?` | Transport or another non-HTTP error generated while attempting request. | +| `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. | + +In addition, `RestResponse` has one additional property: + +| Property | Type | Description | +|----------|------|-----------------------------------------------------------------------------------------------------------------------------------------------------------| +| `Data` | `T?` | Deserialized response object. `Null` if there's no content in the response, deserializer failed to understand the response content, or if request failed. | diff --git a/docs/versioned_docs/version-v113/advanced/_category_.json b/docs/versioned_docs/version-v113/advanced/_category_.json new file mode 100644 index 000000000..f395bdfe4 --- /dev/null +++ b/docs/versioned_docs/version-v113/advanced/_category_.json @@ -0,0 +1,7 @@ +{ + "label": "Advanced topics", + "position": 4, + "link": { + "type": "generated-index" + } +} diff --git a/docs/versioned_docs/version-v113/advanced/authenticators.md b/docs/versioned_docs/version-v113/advanced/authenticators.md new file mode 100644 index 000000000..14196d97c --- /dev/null +++ b/docs/versioned_docs/version-v113/advanced/authenticators.md @@ -0,0 +1,185 @@ +# Authenticators + +RestSharp includes authenticators for basic HTTP, OAuth1 and token-based (JWT and OAuth2). + +There are two ways to set the authenticator: client-wide or per-request. + +Set the client-wide authenticator by assigning the `Authenticator` property of `RestClientOptions`: + +```csharp +var options = new RestClientOptions("https://example.com") { + Authenticator = new HttpBasicAuthenticator("username", "password") +}; +var client = new RestClient(options); +``` + +To set the authenticator per-request, assign the `Authenticator` property of `RestRequest`: + +```csharp +var request = new RestRequest("/api/users/me") { + Authenticator = new HttpBasicAuthenticator("username", "password") +}; +var response = await client.ExecuteAsync(request, cancellationToken); +``` + +## Basic authentication + +The `HttpBasicAuthenticator` allows you pass a username and password as a basic `Authorization` header using a base64 encoded string. + +```csharp +var options = new RestClientOptions("https://example.com") { + Authenticator = new HttpBasicAuthenticator("username", "password") +}; +var client = new RestClient(options); +``` + +## OAuth1 + +For OAuth1 authentication the `OAuth1Authenticator` class provides static methods to help generate an OAuth authenticator. +OAuth1 authenticator will add the necessary OAuth parameters to the request, including signature. + +The authenticator will use `HMAC SHA1` to create a signature by default. +Each static function to create the authenticator allows you to override the default and use another method to generate the signature. + +### Request token + +Getting a temporary request token is the usual first step in the 3-legged OAuth1 flow. +Use `OAuth1Authenticator.ForRequestToken` function to get the request token authenticator. +This method requires a `consumerKey` and `consumerSecret` to authenticate. + +```csharp +var options = new RestClientOptions("https://api.twitter.com") { + Authenticator = OAuth1Authenticator.ForRequestToken(consumerKey, consumerSecret) +}; +var client = new RestClient(options); +var request = new RestRequest("oauth/request_token"); +``` + +The response should contain the token and the token secret, which can then be used to complete the authorization process. +If you need to provide the callback URL, assign the `CallbackUrl` property of the authenticator to the callback destination. + +### Access token + +Getting an access token is the usual third step in the 3-legged OAuth1 flow. +This method retrieves an access token when provided `consumerKey`, `consumerSecret`, `oauthToken`, and `oauthTokenSecret`. +If you don't have a token for this call, you need to make a call to get the request token as described above. + +```csharp +var authenticator = OAuth1Authenticator.ForAccessToken( + consumerKey, consumerSecret, oauthToken, oauthTokenSecret +); +var options = new RestClientOptions("https://api.twitter.com") { + Authenticator = authenticator +}; +var client = new RestClient(options); +var request = new RestRequest("oauth/access_token"); +``` + +If the second step in 3-leg OAuth1 flow returned a verifier value, you can use another overload of `ForAccessToken`: + +```csharp +var authenticator = OAuth1Authenticator.ForAccessToken( + consumerKey, consumerSecret, oauthToken, oauthTokenSecret, verifier +); +``` + +The response should contain the access token that can be used to make calls to protected resources. + +For refreshing access tokens, use one of the two overloads of `ForAccessToken` that accept `sessionHandle`. + +### Protected resource + +When the access token is available, use `ForProtectedResource` function to get the authenticator for accessing protected resources. + +```csharp +var authenticator = OAuth1Authenticator.ForAccessToken( + consumerKey, consumerSecret, accessToken, accessTokenSecret +); +var options = new RestClientOptions("https://api.twitter.com/1.1") { + Authenticator = authenticator +}; +var client = new RestClient(options); +var request = new RestRequest("statuses/update.json", Method.Post) + .AddParameter("status", "Hello Ladies + Gentlemen, a signed OAuth request!") + .AddParameter("include_entities", "true"); +``` + +### xAuth + +xAuth is a simplified version of OAuth1. It allows sending the username and password as `x_auth_username` and `x_auth_password` request parameters and directly get the access token. xAuth is not widely supported, but RestSharp still allows using it. + +Create an xAuth authenticator using `OAuth1Authenticator.ForClientAuthentication` function: + +```csharp +var authenticator = OAuth1Authenticator.ForClientAuthentication( + consumerKey, consumerSecret, username, password +); +``` + +### 0-legged OAuth + +The access token authenticator can be used in 0-legged OAuth scenarios by providing `null` for the `consumerSecret`. + +```csharp +var authenticator = OAuth1Authenticator.ForAccessToken( + consumerKey, null, oauthToken, oauthTokenSecret +); +``` + +## OAuth2 + +RestSharp has two very simple authenticators to send the access token as part of the request. + +`OAuth2UriQueryParameterAuthenticator` accepts the access token as the only constructor argument, and it will send the provided token as a query parameter `oauth_token`. + +`OAuth2AuthorizationRequestHeaderAuthenticator` has two constructors. One only accepts a single argument, which is the access token. The other constructor also allows you to specify the token type. The authenticator will then add an `Authorization` header using the specified token type or `OAuth` as the default token type, and the token itself. + +For example: + +```csharp +var authenticator = new OAuth2AuthorizationRequestHeaderAuthenticator( + token, "Bearer" +); +var options = new RestClientOptions("https://example.com") { + Authenticator = authenticator +}; +var client = new RestClient(options); +``` + +The code above will tell RestSharp to send the bearer token with each request as a header. Essentially, the code above does the same as the sample for `JwtAuthenticator` below. + +As those authenticators don't do much to get the token itself, you might be interested in looking at our [sample OAuth2 authenticator](../usage/example.md#authenticator), which requests the token on its own. + +## JWT + +The JWT authentication can be supported by using `JwtAuthenticator`. It is a very simple class that can be constructed like this: + +```csharp +var authenticator = new JwtAuthenticator(myToken); +var options = new RestClientOptions("https://example.com") { + Authenticator = authenticator +}; +var client = new RestClient(options); +``` + +For each request, it will add an `Authorization` header with the value `Bearer `. + +As you might need to refresh the token from, you can use the `SetBearerToken` method to update the token. + +## Custom authenticator + +You can write your own implementation by implementing `IAuthenticator` and +registering it with your RestClient: + +```csharp +var authenticator = new SuperAuthenticator(); // implements IAuthenticator +var options = new RestClientOptions("https://example.com") { + Authenticator = authenticator +}; +var client = new RestClient(options); +``` + +The `Authenticate` method is the very first thing called upon calling `RestClient.Execute` or `RestClient.Execute`. +It gets the `RestRequest` currently being executed giving you access to every part of the request data (headers, parameters, etc.) + +You can find an example of a custom authenticator that fetches and uses an OAuth2 bearer token [here](../usage/example.md#authenticator). diff --git a/docs/versioned_docs/version-v113/advanced/configuration.md b/docs/versioned_docs/version-v113/advanced/configuration.md new file mode 100644 index 000000000..f15f56d7f --- /dev/null +++ b/docs/versioned_docs/version-v113/advanced/configuration.md @@ -0,0 +1,230 @@ +--- +title: Configuration +description: Learn how to configure RestClient for non-trivial use cases. +sidebar_position: 1 +--- + +# Configuring RestClient + +This page describes how to create and configure `RestClient`. + +## Basic configuration + +The primary `RestClient` constructor accepts an instance of `RestClientOptions`. Most of the time, default option values don't need to be changed. However, in some cases, you'd want to configure the client differently, so you'd need to change some of the options in your code. The constructor also contains a few optional parameters for additional configuration that is not covered by client options. Here's the constructor signature: + +```csharp +public RestClient( + RestClientOptions options, + ConfigureHeaders? configureDefaultHeaders = null, + ConfigureSerialization? configureSerialization = null, + bool useClientFactory = false +) +``` + +Constructor parameters are: + +| Name | Description | Mandatory | +|-------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------| +| options | Client options | Yes | +| configureDefaultHeaders | Function to configure headers. Allows to configure default headers for `HttpClient`. Most of the time you'd prefer using `client.AddDefaultHeader` instead. | No | +| configureSerialization | Function to configure client serializers with non-default options or to use a different serializer ([learn more](serialization.md)) | No | +| useClientFactory | Instructs the client to use `SimpleFactory` ([learn more](../usage/client.md#simple-factory)) to get an `HttpClient` instance | No | + +Here's an example of how to create a client using client options: + +```csharp +var options = new RestClientOptions("https://localhost:5000/api") { + DisableCharset = true +}; +var client = new RestClient(options); +``` + +When you only need to set the base URL, you can use a simplified constructor: + +```csharp +var client = new RestClient("https://localhost:5000/api"); +``` + +The simplified constructor will create an instance of client options and set the base URL provided as the constructor argument. + +Finally, you can override properties of default options using a configuration function. Here's the constructor signature that supports this method: + +```csharp +public RestClient( + ConfigureRestClient? configureRestClient = null, + ConfigureHeaders? configureDefaultHeaders = null, + ConfigureSerialization? configureSerialization = null, + bool useClientFactory = false +) +``` + +For example: + +```csharp +var client = new RestClient(options => { + options.BaseUrl = new Uri("https://localhost:5000/api"); + options.DisableCharset = true +}); +``` + +You can also provide the base URL as a constructor argument like this: + +```csharp +var client = new RestClient("https://localhost:5000/api", options => { + options.DisableCharset = true +}); +``` + +## Using custom HttpClient + +By default, RestSharp creates an instance of `HttpClient` configured using the client options, and keeps it during the lifetime of the client. When the `RestClient` instance gets disposed, it also disposes the `HttpClient` instance. + +There might be a case when you need to provide your own `HttpClient`. For example, you would want to use `HttpClient` created by HTTP client factory. RestSharp allows you to do it by using additional constructors. These constructors are: + +```csharp +// Create a client using an existing HttpClient and RestClientOptions (optional) +public RestClient( + HttpClient httpClient, + RestClientOptions? options, + bool disposeHttpClient = false, + ConfigureSerialization? configureSerialization = null +) + +// Create a client using an existing HttpClient and optional RestClient configuration function +public RestClient( + HttpClient httpClient, + bool disposeHttpClient = false, + ConfigureRestClient? configureRestClient = null, + ConfigureSerialization? configureSerialization = null +) +``` + +The `disposeHttpClient` argument tells the client to dispose `HttpClient` when the client itself gets disposed. It's set to `false` by default as when the `HttpClient` is provided from the outside, it should normally be disposed on the outside as well. + +## Using custom message handler + +Unless you use an external instance of `HttpClient`, the `RestClient` creates one when being constructed, and it will use the default HTTP message handler, configured using `RestClientOptions`. Normally, you'd get a `SocketHttpHandler` with modern .NET, and `WinHttpHandler` with .NET Framework. + +There might be a case when you need to configure the HTTP message handler. For example, you want to add a delegating message handler. RestSharp allows you to do it by using additional constructors. There's one constructor that allows you to pass the custom `HttpMessageHandler`: + +```csharp +public RestClient( + HttpMessageHandler handler, + bool disposeHandler = true, + ConfigureRestClient? configureRestClient = null, + ConfigureSerialization? configureSerialization = null +) +``` + +This constructor will create a new `HttpClient` instance using the provided message handler. As RestSharp will dispose the `HttpClient` instance when the `RestClient` instance gets disposed, the handler will be disposed as well. If you want to change that and keep the handler, set the `disposeHandler` parameter to `false`. + +:::note +When using a custom message handler, RestSharp **will not** configure it with client options, which are normally used to configure the handler created by RestSharp. +::: + +Another way to customize the message handler is to allow RestSharp to create a handler, but then configure it, or wrap it in a delegating handler. It can be done by using the `RestClientOptions.ConfigureMessageHandler` property. It can be set to a function that receives the handler created by RestSharp and returned either the same handler with different settings, or a new handler. + +For example, if you want to use `MockHttp` and its handler for testing, you can do it like this: + +```csharp +var mockHttp = new MockHttpMessageHandler(); +// Configure the MockHttp handler to do the checks +... + +var options = new RestClientOptions(Url) { + ConfigureMessageHandler = _ => mockHttp +}; +using var client = new RestClient(options); +``` + +In this example, we are reassigning the handler to MockHttp, so the handler created by RestSharp isn't used. In other cases you want to use delegating handlers as middleware, so you'd pass the handler created by RestSharp to the delegating handler: + +```csharp +var options = new RestClientOptions(Url) { + ConfigureMessageHandler = handler => new MyDelegatingHandler(handler) +}; +using var client = new RestClient(options); +``` + +## Client options + +RestSharp allows configuring `RestClient` using client options, as mentioned at the beginning of this page. Below, you find more details about available options. + +| Option | Description | +|----------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `BaseUrl` | Client base URL. It can also be provided as the `RestClientOptions` constructor argument. | +| `ConfigureMessageHandler` | Configures the HTTP message handler (see above). | +| `CalculateResponseStatus` | Function to calculate a different response status from `HttpResponseMessage`. By default, the request is considered as complete if it returns a successful status code or 404. | +| `Authenticator` | Client-level authenticator. Read more about authenticators [here](authenticators.md). | +| `Interceptors` | A collector of interceptors. Read more about interceptors [here](interceptors.md). | +| `Credentials` | Instance of `ICredentials` used for NTLM or Kerberos authentication. Not supported in browsers. | +| `UseDefaultCredentials` | Whether to use default OS credentials for NTLM or Kerberos authentication. Not supported in browsers. | +| `DisableCharset` | When set to `true`, the `Content-Type` header won't have the `charset` portion. Some older web servers don't understand the `charset` portion in the header and fail to process the request. | +| `AutomaticDecompression` | Allows customizing supported decompression methods. Default is `All` except for .NET Framework that only support `GZip`. Not supported in browsers. | +| `MaxRedirects` | The number of redirects to follow. Not supported in browsers. | +| `ClientCertificates` | A collection of X.509 client certificates to be used for authentication. Not supported in browsers. | +| `Proxy` | Can be used if the client needs to use an explicit, non-default proxy. Not supported in browsers, on iOS and tvOS. | +| `CachePolicy` | Shortcut for setting the default value for `Cache-Control` header. | +| `FollowRedirects` | Instructs the client to follow redirects. Default is `true`. | +| `Expect100Continue` | Gets or sets a value that indicates if the `Expect` header for an HTTP request contains `Continue`. | +| `UserAgent` | Allows overriding the default value for `User-Agent` header, which is `RestSharp/{version}`. | +| `PreAuthenticate` | Gets or sets a value that indicates whether the client sends an `Authorization` header with the request. Not supported in browsers. | +| `RemoteCertificateValidationCallback` | Custom function to validate the server certificate. Normally, it's used when the server uses a certificate that isn't trusted by default. | +| `BaseHost` | Value for the `Host` header sent with each request. | +| `CookieContainer` | Custom cookie container that will be shared among all calls made by the client. Normally not required as RestSharp handles cookies without using a client-level cookie container. | +| `MaxTimeout` | Client-level timeout in milliseconds. If the request timeout is also set, this value isn't used. | +| `Encoding` | Default request encoding. Override it only if you don't use UTF-8. | +| `ThrowOnDeserializationError` | Forces the client to throw if it fails to deserialize the response. Remember that not all deserialization issues forces the serializer to throw. Default is `false`, so the client will return a `RestResponse` with deserialization exception details. Only relevant for `Execute...` functions. | +| `FailOnDeserializationError` | When set to `true`, if the client fails to deserialize the response, the response object will have status `Failed`, although the HTTP calls might have been successful. Default is `true`. | +| `ThrowOnAnyError` | When set to `true`, the client will re-throw any exception from `HttpClient`. Default is `false`. Only applies for `Execute...` functions. | +| `AllowMultipleDefaultParametersWithSameName` | By default, adding parameters with the same name is not allowed. You can override this behaviour by setting this property to `true`. | +| `Encode` | A function to encode URLs, the default is a custom RestSharp function based on `Uri.EscapeDataString()`. Set it if you need a different way to do the encoding. | +| `EncodeQuery` | A function to encode URL query parameters. The default is the same function as for `Encode` property. | + +Some of the options are used by RestSharp code, but some are only used to configure the `HttpMessageHandler`. These options are: +- `Credentials` +- `UseDefaultCredentials` +- `AutomaticDecompression` +- `PreAuthenticate` +- `MaxRedirects` +- `RemoteCertificateValidationCallback` +- `ClientCertificates` +- `FollowRedirects` +- `Proxy` + +:::note +If setting these options to non-default values produce no desirable effect, check if your framework and platform supports them. RestSharp doesn't change behaviour based on values of those options. +::: + +The `IRestClient` interface exposes the `Options` property, so any option can be inspected at runtime. However, RestSharp converts the options object provided to the client constructor to an immutable object. Therefore, no client option can be changed after the client is instantiated. It's because changing client options at runtime can produce issues in concurrent environments, effectively rendering the client as not thread-safe. Apart from that, changing the options that are used to create the message handler would require re-creating the handler, and also `HttpClient`, which should not be done at runtime. + +## Configuring requests + +Client options apply to all requests made by the client. Sometimes, you want to fine-tune particular requests, so they execute with custom configuration. It's possible to do using properties of `RestRequest`, described below. + +| Name | Description | +|------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `AlwaysMultipartFormData` | When set to `true`, the request will be sent as a multipart form, even though it's not required. By default, RestSharp only sends requests with multiple attachments as multipart forms. Default is `false`. | +| `AlwaysSingleFileAsContent` | When set to true, the request with file attachment will not be sent as a multipart form, but as plain content. Default is `false`. It cannot be set to `true` when `AlwaysMultipartFormData` is set to `true`, or when the request has `POST` parameters. | +| `MultipartFormQuoteBoundary` | Default is `true`, which means that the form boundary string will be wrapped in quotes. If the server has an issue with that, setting this to `false` will remove quotes around the boundary. | +| `FormBoundary` | Allows specifying a custom multipart form boundary instead of using the default random string. | +| `RequestParameters` | Collection of request parameters. Normally, you won't need to use it as parameters are added to the request using `Add...` functions. | +| `CookieContainer` | Custom request-level cookie container. Default is `null`. You can still set request cookies using `AddCookie` and get response cookies from the response object without using cooking container. | +| `Authenticator` | Overrides the client-level authenticator. | +| `Files` | Collection of file parameters, read-only. Use `AddFile` for adding files to the request. | +| `Method` | Request HTTP method, default is `GET`. Only needed when using `Execute` or `ExecuteAsync` as other functions like `ExecutePostAsync` will override the request method. | +| `TImeout` | Overrides the client-level timeout. | +| `Resource` | Resource part of the remote endpoint URL. For example, when using the client-level base URL `https://localhost:5000/api` and `Resource` set to `weather`, the request will be sent to `https://localhost:5000/api/weather`. It can container resource placeholders to be used in combination with `AddUrlSegment` | +| `RequestFormat` | Identifies the request as JSON, XML, binary, or none. Rarely used because the client will set the request format based on the body type if functions like `AddJsonBody` or `AddXmlBody` are used. | +| `RootElement` | Used by the default deserializers to determine where to start deserializing from. Only supported for XML responses. Does not apply to requests. | +| `OnBeforeDeserialization` | **Obsolete** A function to be called before the response is deserializer. Allows changing the content before calling the deserializer. Use [interceptors](interceptors.md) instead. | +| `OnBeforeRequest` | **Obsolete** A function to be called right before the request is executed by `HttpClient`. It receives an instance of `HttpRequestMessage`. Use [interceptors](interceptors.md) instead. | +| `OnAfterRequest` | **Obsolete** A function to be called right after the request is executed by `HttpClient`. It receives an instance of `HttpResponseMessage`. Use [interceptors](interceptors.md) instead. | +| `Attempts` | When the request is being resent to retry, the property value increases by one. | +| `CompletionOption` | Instructs the client on when it should consider the request to be completed. The default is `ResponseContentRead`. It is automatically changed to `ResponseHeadersRead` when using async download functions or streaming. | +| `CachePolicy` | Overrides the client cache policy. | +| `ResponseWriter` | Allows custom handling of the response stream. The function gets the raw response stream and returns another stream or `null`. Cannot be used in combination with `AdvancedResponseWriter`. | +| `AdvancedResponseWriter` | Allows custom handling of the response. The function gets an instance of `HttpResponseMessage` and an instance of `RestRequest`. It must return an instance of `RestResponse`, so it effectively overrides RestSharp default functionality for creating responses. | +| `Interceptors` | Allows adding interceptors to the request. Both client-level and request-level interceptors will be called. | + +The table below contains all configuration properties of `RestRequest`. To learn more about adding request parameters, check the [usage page](../usage/request.md) page about creating requests with parameters. diff --git a/docs/versioned_docs/version-v113/advanced/error-handling.md b/docs/versioned_docs/version-v113/advanced/error-handling.md new file mode 100644 index 000000000..80e822dcc --- /dev/null +++ b/docs/versioned_docs/version-v113/advanced/error-handling.md @@ -0,0 +1,71 @@ +# Error handling + +If there is a network transport error (network is down, failed DNS lookup, etc.), or any kind of server error (except 404), `RestResponse.ResponseStatus` will be set to `ResponseStatus.Error`, otherwise it will be `ResponseStatus.Completed`. + +If an API returns a 404, `ResponseStatus` will still be `Completed`. If you need access to the HTTP status code returned, you will find it at `RestResponse.StatusCode`. +The `Status` property is an indicator of completion independent of the API error handling. + +Normally, RestSharp doesn't throw an exception if the request fails. + +However, it is possible to configure RestSharp to throw in different situations when it normally doesn't throw +in favor of giving you the error as a property. + +| Property | Behavior | +|-------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `FailOnDeserializationError` | Changes the default behavior when failed deserialization results in a successful response with an empty `Data` property of the response. Setting this property to `true` will tell RestSharp to consider failed deserialization as an error and set the `ResponseStatus` to `Error` accordingly. | +| `ThrowOnDeserializationError` | Changes the default behavior when failed deserialization results in empty `Data` property of the response. Setting this property to `true` will tell RestSharp to throw when deserialization fails. | +| `ThrowOnAnyError` | Setting this property to `true` changes the default behavior and forces RestSharp to throw if any errors occurs when making a request or during deserialization. | + +Those properties are available for the `RestClientOptions` and will be used for all request made with the client instance. + +For example, you can configure the client to throw an exception if any error occurs when making a request or when a request returns a non-successful HTTP status code: + +```csharp +var options = new RestClientOptions(url) { + ThrowOnAnyError = true +}; +var client = new RestClient(options); +var request = new RestRequest("resource/{id}").AddUrlSegment("id", 123); + +// 👇 will throw if the request fails +var deserialized = await client.GetAsync(request); + +// 👇 will NOT throw if the request fails, inspect the response to find out what happened +var response = await client.ExecuteGetAsync(request); +``` + +:::warning +Please be aware that deserialization failures will only work if the serializer throws an exception when deserializing the response. +Many serializers don't throw by default, and just return a `null` result. RestSharp is unable to figure out why `null` is returned, so it won't fail in this case. +Check the serializer documentation to find out if it can be configured to throw on deserialization error. +::: + +There are also slight differences on how different overloads handle exceptions. + +Asynchronous generic methods `GetAsync`, `PostAsync` and so on, which aren't a part of `RestClient` API (those methods are extension methods) return `Task`. It means that there's no `RestResponse` to set the response status to error. We decided to throw an exception when such a request fails. It is a trade-off between the API consistency and usability of the library. Usually, you only need the content of `RestResponse` instance to diagnose issues and most of the time the exception would tell you what's wrong. + +Below, you can find how different extensions deal with errors. Note that functions, which don't throw by default, will throw exceptions when `ThrowOnAnyError` is set to `true`. + +| Function | Throws on errors | +|:----------------------|:-----------------| +| `ExecuteAsync` | No | +| `ExecuteGetAsync` | No | +| `ExecuteGetAsync` | No | +| `ExecutePostAsync` | No | +| `ExecutePostAsync` | No | +| `ExecutePutAsync` | No | +| `ExecutePutAsync` | No | +| `GetAsync` | Yes | +| `GetAsync` | Yes | +| `PostAsync` | Yes | +| `PostAsync` | Yes | +| `PatchAsync` | Yes | +| `PatchAsync` | Yes | +| `DeleteAsync` | Yes | +| `DeleteAsync` | Yes | +| `OptionsAsync` | Yes | +| `OptionsAsync` | Yes | +| `HeadAsync` | Yes | +| `HeadAsync` | Yes | + +In addition, all the functions for JSON requests, like `GetJsonAsync` and `PostJsonAsync` throw an exception if the HTTP call fails. diff --git a/docs/versioned_docs/version-v113/advanced/interceptors.md b/docs/versioned_docs/version-v113/advanced/interceptors.md new file mode 100644 index 000000000..a3bf7a9bc --- /dev/null +++ b/docs/versioned_docs/version-v113/advanced/interceptors.md @@ -0,0 +1,84 @@ +--- +title: Interceptors +--- + +## Intercepting requests and responses + +Interceptors are a powerful feature of RestSharp that allows you to modify requests and responses before they are sent or received. You can use interceptors to add headers, modify the request body, or even cancel the request. You can also use interceptors to modify the response before it is returned to the caller. + +### Implementing an interceptor + +To implement an interceptor, you need to create a class that inherits the `Interceptor` base class. The base class implements all interceptor methods as virtual, so you can override them in your derived class. + +Methods that you can override are: +- `BeforeRequest(RestRequest request, CancellationToken cancellationToken)` +- `AfterRequest(RestResponse response, CancellationToken cancellationToken)` +- `BeforeHttpRequest(HttpRequestMessage requestMessage, CancellationToken cancellationToken)` +- `AfterHttpRequest(HttpResponseMessage responseMessage, CancellationToken cancellationToken)` +- `BeforeDeserialization(RestResponse response, CancellationToken cancellationToken)` + +All those functions must return a `ValueTask` instance. + +Here's an example of an interceptor that adds a header to a request: + +```csharp +// This interceptor adds a header to the request +// You'd not normally use this interceptor, as RestSharp already has a method +// to add headers to the request +class HeaderInterceptor(string headerName, string headerValue) : Interceptors.Interceptor { + public override ValueTask BeforeHttpRequest(HttpRequestMessage requestMessage, CancellationToken cancellationToken) { + requestMessage.Headers.Add(headerName, headerValue); + return ValueTask.CompletedTask; + } +} +``` + +Because interceptor functions return `ValueTask`, you can use `async` and `await` inside them. + +### Using an interceptor + +It's possible to add as many interceptors as you want, both to the client and to the request. The interceptors are executed in the order they were added. + +Adding interceptors to the client is done via the client options: + +```csharp +var options = new RestClientOptions("https://api.example.com") { + Interceptors = [new HeaderInterceptor("Authorization", token)] +}; +var client = new RestClient(options); +``` + +When you add an interceptor to the client, it will be executed for every request made by that client. + +You can also add an interceptor to a specific request: + +```csharp +var request = new RestRequest("resource") { + Interceptors = [new HeaderInterceptor("Authorization", token)] +}; +``` + +In this case, the interceptor will only be executed for that specific request. + +### Deprecation notice + +Interceptors aim to replace the existing request hooks available in RestSharp prior to version 111.0. Those hooks are marked with `Obsolete` attribute and will be removed in the future. If you are using those hooks, we recommend migrating to interceptors as soon as possible. + +To make the migration easier, RestSharp provides a class called `CompatibilityInterceptor`. It has properties for the hooks available in RestSharp 110.0 and earlier. You can use it to migrate your code to interceptors without changing the existing logic. + +For example, a code that uses `OnBeforeRequest` hook: + +```csharp +var request = new RestRequest("success"); +request.OnBeforeDeserialization += _ => throw new Exception(exceptionMessage); +``` + +Can be migrated to interceptors like this: + +```csharp +var request = new RestRequest("success") { + Interceptors = [new CompatibilityInterceptor { + OnBeforeDeserialization = _ => throw new Exception(exceptionMessage) + }] +}; +``` \ No newline at end of file diff --git a/docs/versioned_docs/version-v113/advanced/serialization.md b/docs/versioned_docs/version-v113/advanced/serialization.md new file mode 100644 index 000000000..b7092519a --- /dev/null +++ b/docs/versioned_docs/version-v113/advanced/serialization.md @@ -0,0 +1,148 @@ +# Serialization + +One of the most common reasons to choose RestSharp over plain `HttpClient` is its rich build-in serialization support. RestSharp allows adding complex objects as request body to be serialized when making a call to an API endpoint, and deserializing the response to a given .NET type. RestSharp supports JSON and XML serialization and deserialization by default. In addition, you can use a CSV serializer or write your own. + +In contrast to `System.Net.Http.Json` package that contains `HttpClient` extensions to make `GET` or `POST` calls using JSON, RestSharp support JSON responses for all HTTP methods, not just for `GET`. + +## Configuration + +:::tip +The default behavior of RestSharp is to swallow deserialization errors and return `null` in the `Data` +property of the response. Read more about it in the [Error Handling](error-handling.md). +::: + +You can tell RestSharp to use a custom serializer by using the `configureSerialization` constructor parameter: + +```csharp +var client = new RestClient( + options, + configureSerialization: s => s.UseSerializer(() => new CustomSerializer()); +); +``` + +All RestSharp serializers implement the `IRestSerializer` interface. Among other things, the interface requires implementing the `AcceptedContentTypes` property, which must return a collection of content types supported by the serializer. Being configured to use certain serializers, RestSharp populates the `Accept` header accordingly, so it doesn't need to be set manually. + +When making a call, RestSharp sets the request content type according to the request body type. For example, when you use `AddJsonBody`, the content type is set to `application/json`. Normally, you won't need to set the `Content-Type` header manually. If you need to set a custom content type for a JSON call, you can use the optional `contentType` argument of `AddJsonBody`, for example: + +```csharp +request.AddJsonBody(data, "text/json"); +``` + +## JSON + +The default JSON serializer uses `System.Text.Json`, which is a part of .NET since .NET 6. For earlier versions, it is added as a dependency. There are also a few serializers provided as additional packages. + +By default, RestSharp will use `JsonSerializerDefaults.Web` configuration. If necessary, you can specify your own options: + +```csharp +var client = new RestClient( + options, + configureSerialization: s => s.UseSystemTextJson(new JsonSerializerOptions {...}) +); +``` + +## XML + +The default XML serializer is `DotNetXmlSerializer`, which uses `System.Xml.Serialization` library from .NET. + +In previous versions of RestSharp, the default XML serializer was a custom RestSharp XML serializer. To make the code library size smaller, that serializer is now available as a separate package [`RestSharp.Serializers.Xml`](https://www.nuget.org/packages/RestSharp.Serializers.Xml). +You can add it back if necessary by installing the package and adding it to the client: + +```csharp +var client = new RestClient( + options, + configureSerialization: s => s.UseXmlSerializer() +); +``` + +As before, you can supply three optional arguments for a custom namespace, custom root element, and if you want to use `SerializeAs` and `DeserializeAs` attributed. + +## NewtonsoftJson (aka Json.Net) + +The `NewtonsoftJson` package is the most popular JSON serializer for .NET. It handles all possible scenarios and is very configurable. Such a flexibility comes with the cost of performance. If you need speed, keep the default JSON serializer. + +RestSharp support Json.Net serializer via a separate package [`RestSharp.Serializers.NewtonsoftJson`](https://www.nuget.org/packages/RestSharp.Serializers.NewtonsoftJson). + +:::warning +Please note that `RestSharp.Newtonsoft.Json` package is not provided by RestSharp, is marked as obsolete on NuGet, and no longer supported by its creator. +::: + +Use the extension method provided by the package to configure the client: + +```csharp +var client = new RestClient( + options, + configureSerialization: s => s.UseNewtonsoftJson() +); +``` + +The serializer configures some options by default: + +```csharp +JsonSerializerSettings DefaultSettings = new JsonSerializerSettings { + ContractResolver = new CamelCasePropertyNamesContractResolver(), + DefaultValueHandling = DefaultValueHandling.Include, + TypeNameHandling = TypeNameHandling.None, + NullValueHandling = NullValueHandling.Ignore, + Formatting = Formatting.None, + ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor +}; +``` + +If you need to use different settings, you can supply your instance of +`JsonSerializerSettings` as a parameter for the extension method. + +## CSV + +A separate package `RestSharp.Serializers.CsvHelper` provides a CSV serializer for RestSharp. It is based on the +`CsvHelper` library. + +Use the extension method provided by the package to configure the client: + +```csharp +var client = new RestClient( + options, + configureSerialization: s => s.UseCsvHelper() +); +``` + +You can also supply your instance of `CsvConfiguration` as a parameter for the extension method. + +```csharp +var client = new RestClient( + options, + configureSerialization: s => s.UseCsvHelper( + new CsvConfiguration(CultureInfo.InvariantCulture) {...} + ) +); +``` + +## Custom + +You can also implement your custom serializer. To support both serialization and +deserialization, you must implement the `IRestSerializer` interface. + +Here is an example of a custom serializer that uses `System.Text.Json`: + +```csharp +public class SimpleJsonSerializer : IRestSerializer { + public string? Serialize(object? obj) => obj == null ? null : JsonSerializer.Serialize(obj); + + public string? Serialize(Parameter bodyParameter) => Serialize(bodyParameter.Value); + + public T? Deserialize(RestResponse response) => JsonSerializer.Deserialize(response.Content!); + + public ContentType ContentType { get; set; } = ContentType.Json; + + public ISerializer Serializer => this; + public IDeserializer Deserializer => this; + public DataFormat DataFormat => DataFormat.Json; + public string[] AcceptedContentTypes => ContentType.JsonAccept; + public SupportsContentType SupportsContentType + => contentType => contentType.Value.EndsWith("json", StringComparison.InvariantCultureIgnoreCase); +} +``` + +The `SupportedContentTypes` function will be used to check if the serializer is able to deserialize the response based on the `Content-Type` response header. + +The `ContentType` property will be used when making a request so the server knows how to handle the payload. diff --git a/docs/versioned_docs/version-v113/changelog.md b/docs/versioned_docs/version-v113/changelog.md new file mode 100644 index 000000000..ea7346cf5 --- /dev/null +++ b/docs/versioned_docs/version-v113/changelog.md @@ -0,0 +1,29 @@ +--- +title: What's new +description: List of changes for the current major version +sidebar_position: 1 +--- + +# Changelog + +For release notes of previous versions, please check the [Releases page](https://github.com/restsharp/RestSharp/releases) in RestSharp GitHub repository. + +Changes between major versions are documented in the documentation for each version on this website. + +# v112.0 + +* Security fix for [CVE-2024-45302](https://github.com/restsharp/RestSharp/security/advisories/GHSA-4rr6-2v9v-wcpc). Header values cannot contain `CRLF`. + +## v112.1 + +* Follow up on v112.0 security fix: remove `\t` from the list of forbidden characters in headers. + +# v113.0 + +* Added support for .NET 9 and .NET 10 +* Upgraded `System.Text.Json` to v10 for all target frameworks +* Added support for Microsoft dependency injection and HTTP client factory +* For responses with 404 (not found) status code the `IsSuccessful` is set to `false` +* When the new option `ErrorWhenUnsuccessfulStatusCode` is set to `false`, the error message and the exception won't be added to the response. Default for this option is `true` for backwards compatibility. +* When `AddUrlSegment` is called more than once with the same name, the last value will be used. +* The new package `RestSharp.Extensions.DependencyInjection` integrates RestSharp with Microsoft DI container and `IHttpClientFactory`. \ No newline at end of file diff --git a/docs/versioned_docs/version-v113/intro.md b/docs/versioned_docs/version-v113/intro.md new file mode 100644 index 000000000..961c5350a --- /dev/null +++ b/docs/versioned_docs/version-v113/intro.md @@ -0,0 +1,112 @@ +--- +sidebar_position: 2 +title: Quick start +--- + +## Introduction + +:::warning +RestSharp v107+ changes the library API surface and its behaviour significantly. We advise looking at [migration](/migration) docs to understand how to migrate to the latest version of RestSharp. +::: + +The main purpose of RestSharp is to make synchronous and asynchronous calls to remote resources over HTTP. As the name suggests, the main audience of RestSharp are developers who use REST APIs. However, RestSharp can call any API over HTTP, as long as you have the resource URI and request parameters that you want to send comply with W3C HTTP standards. + +One of the main challenges of using HTTP APIs for .NET developers is to work with requests and responses of different kinds and translate them to complex C# types. RestSharp can take care of serializing the request body to JSON or XML and deserialize the response. It can also form a valid request URI based on different parameter kinds: path, query, form or body. + +## Getting Started + +Before you can use RestSharp in your application, you need to add the NuGet package. You can do it using your IDE or the command line: + +``` +dotnet add package RestSharp +``` + +### Basic Usage + +If you only have a small number of one-off API requests to perform, you can use RestSharp like this: + +```csharp +using RestSharp; +using RestSharp.Authenticators; + +var options = new RestClientOptions("https://api.twitter.com/1.1") { + Authenticator = new HttpBasicAuthenticator("username", "password") +}; +var client = new RestClient(options); +var request = new RestRequest("statuses/home_timeline.json"); +// The cancellation token comes from the caller. You can still make a call without it. +var response = await client.GetAsync(request, cancellationToken); +``` + +It will return a `RestResponse` back, which contains all the information returned from the remote server. +You have access to the headers, content, HTTP status and more. + +You can also use generic overloads like `Get` to automatically deserialize the response into a .NET class. + +For example: + +```csharp +using RestSharp; +using RestSharp.Authenticators; + +var options = new RestClientOptions("https://api.twitter.com/1.1") { + Authenticator = new HttpBasicAuthenticator("username", "password") +}; +var client = new RestClient(options); + +var request = new RestRequest("statuses/home_timeline.json"); + +// The cancellation token comes from the caller. You can still make a call without it. +var timeline = await client.GetAsync(request, cancellationToken); +``` + +Both snippets above use the `GetAsync` extension, which is a wrapper about `ExecuteGetAsync`, which, in turn, is a wrapper around `ExecuteAsync`. +All `ExecuteAsync` overloads and return the `RestResponse` or `RestResponse`. + +The most important difference is that async methods named after HTTP methods (like `GetAsync` or `PostAsync`) return `Task` instead of `Task>`. It means that you won't get an error response if the request fails as those methods throw an exception for unsuccessful HTTP calls. For keeping the API consistent, non-generic functions like `GetAsync` or `PostAsync` also throw an exception if the request fails, although they return the `Task`. + +Read [here](advanced/error-handling.md) about how RestSharp handles exceptions. + +RestSharp also offers simple ways to call APIs that accept and return JSON payloads. You can use the `GetJsonAsync` and `PostJsonAsync` extension methods, which will automatically serialize the request body to JSON and deserialize the response to the specified type. + +```csharp +var client = new RestClient(options); +var timeline = await client.GetJsonAsync("statuses/home_timeline.json", cancellationToken); +``` + +Read [here](usage/execute.md#json-requests) about making JSON calls without preparing a request object. + +### Content type + +RestSharp supports sending XML or JSON body as part of the request. To add a body to the request, simply call `AddJsonBody` or `AddXmlBody` method of the `RestRequest` object. + +There is no need to set the `Content-Type` or add the `DataFormat` parameter to the request when using those methods, RestSharp will do it for you. + +RestSharp will also handle both XML and JSON responses and perform all necessary deserialization tasks, depending on the server response type. Therefore, you only need to add the `Accept` header if you want to deserialize the response manually. + +For example, only you'd only need these lines to make a request with JSON body: + +```csharp +var request = new RestRequest("address/update").AddJsonBody(updatedAddress); +var response = await client.PostAsync(request); +``` + +It's also possible to make the same call using `PostAsync` shorter syntax: + +```csharp +var response = await PostJsonAsync( + "address/update", request, cancellationToken +); +``` + +Read more about serialization and deserialization [here](advanced/serialization.md). + +### Response + +When you use `ExecuteAsync`, you get an instance of `RestResponse` back. The response object has the `Content` property, which contains the response as string. You can find other useful properties there, like `StatusCode`, `ContentType` and so on. If the request wasn't successful, you'd get a response back with `IsSuccessful` property set to `false` and the error explained in the `ErrorException` and `ErrorMessage` properties. + +When using typed `ExecuteAsync`, you get an instance of `RestResponse` back, which is identical to `RestResponse` but also contains the `T Data` property with the deserialized response. + +None of `ExecuteAsync` overloads throw if the remote server returns an error. You can inspect the response and find the status code, error message, and, potentially, an exception. + +Extensions like `GetAsync` will not return the whole `RestResponse` but just a deserialized response. These extensions will throw an exception if the remote server returns an error. The exception details contain the status code returned by the server. diff --git a/docs/versioned_docs/version-v113/usage/_category_.json b/docs/versioned_docs/version-v113/usage/_category_.json new file mode 100644 index 000000000..bd2045cd6 --- /dev/null +++ b/docs/versioned_docs/version-v113/usage/_category_.json @@ -0,0 +1,7 @@ +{ + "label": "Using RestSharp", + "position": 3, + "link": { + "type": "generated-index" + } +} diff --git a/docs/versioned_docs/version-v113/usage/basics.md b/docs/versioned_docs/version-v113/usage/basics.md new file mode 100644 index 000000000..6bbb5594a --- /dev/null +++ b/docs/versioned_docs/version-v113/usage/basics.md @@ -0,0 +1,23 @@ +--- +sidebar_position: 2 +--- + +# RestSharp basics + +This page describes some of the essential properties and features of RestSharp. + +## What RestSharp does + +Essentially, RestSharp is a wrapper around `HttpClient` that allows you to do the following: +- Add default parameters of any kind (not just headers) to the client, once +- Add parameters of any kind to each request (query, URL segment, form, attachment, serialized body, header) in a straightforward way +- Serialize the payload to JSON or XML if necessary +- Set the correct content headers (content type, disposition, length, etc.) +- Handle the remote endpoint response +- Deserialize the response from JSON or XML if necessary + +## API client + +The best way to call an external HTTP API is to create a typed client, which encapsulates RestSharp calls and doesn't expose the `RestClient` instance in public. + +You can find an example of a Twitter API client on the [Example](example.md) page. diff --git a/docs/versioned_docs/version-v113/usage/client.md b/docs/versioned_docs/version-v113/usage/client.md new file mode 100644 index 000000000..bdcc10727 --- /dev/null +++ b/docs/versioned_docs/version-v113/usage/client.md @@ -0,0 +1,123 @@ +--- +sidebar_position: 3 +title: Creating the client +--- + +## Constructors + +A RestSharp client can be instantiated by one of its constructors. Two most commonly used constructors are: + +#### Only specify the base URL + +You can create an instance of `RestClient` with only a single parameter: the base URL. Even that isn't required as base URL can be left empty. In that case, you'd need to specify the absolute path for each call. When the base URL is set, you can use both relative and absolute path. + +```csharp +// Creates a client with default options to call a given base URL +var client = new RestClient("https://localhost:5000"); +``` + +#### Provide client options + +The most common way to create a client is to use the constructor with options. The options object has the type of `RestClientOptions`. +Here's an example of how to create a client using the same base path as in the previous sample, but with a couple additional settings: + +```csharp +// Creates a client using the options object +var options = new RestClientOptions("https://localhost:5000") { + MaxTimeout = 1000 +}; +var client = new RestClient(options); +``` + +#### Advanced configuration + +RestSharp can be configured with more tweaks, including default request options, how it should handle responses, how serialization works, etc. You can also provide your own instance of `HttpClient` or `HttpMessageHandler`. + +Read more about the advanced configuration of RestSharp on a [dedicated page](../advanced/configuration.md). + +## Simple factory + +Another way to create the client instance is to use a simple client factory. The factory will use the `BaseUrl` property of the client options to cache `HttpClient` instances. Every distinct base URL will get its own `HttpClient` instance. Other options don't affect the caching. Therefore, if you use different options for the same base URL, you'll get the same `HttpClient` instance, which will not be configured with the new options. Options that aren't applied _after_ the first client instance is created are: + +* `Credentials` +* `UseDefaultCredentials` +* `AutomaticDecompression` +* `PreAuthenticate` +* `FollowRedirects` +* `RemoteCertificateValidationCallback` +* `ClientCertificates` +* `MaxRedirects` +* `MaxTimeout` +* `UserAgent` +* `Expect100Continue` + +Constructor parameters to configure the `HttpMessageHandler` and default `HttpClient` headers configuration are also ignored for the cached instance as the factory only configures the handler once. + +You need to set the `useClientFactory` parameter to `true` in the `RestClient` constructor to enable the factory. + +```csharp +var options = new RestClientOptions("https://api.twitter.com/2"); +var client = new RestClient(options, useClientFactory: true); +``` + +## Dependency Injection + +Since version 113.0, RestSharp provides integration with Microsoft Dependency Injection container using `RestSharp.Extensions.DepndencyInjection` package. It also allows using `IHttpClientFactory`, which manages the lifecycle of HTTP message handlers, avoiding socket exhaustion when RestClient is used as a transient dependency. It also resolves the issue with DNS changes when RestClient is used as a singleton. The latter is solved by using RestClient as a transient dependency and letting the client factory to deal with message handlers lifecycle. + +Learn more about registering the client in the DI container [here](./di.md). + +## Reusing HttpClient + +RestSharp uses `HttpClient` internally to make HTTP requests. It's possible to reuse the same `HttpClient` instance for multiple `RestClient` instances. This is useful when you want to share the same connection pool between multiple `RestClient` instances. + +One way of doing it is to use `RestClient` constructors that accept an instance of `HttpClient` or `HttpMessageHandler` as an argument. Note that in that case not all the options provided via `RestClientOptions` will be used. Here is the list of options that will work: + +- `BaseAddress` is be used to set the base address of the `HttpClient` instance if base address is not set there already. +- `MaxTimeout` is used to cancel the call using the cancellation token source, so +- `UserAgent` will be added to the `RestClient.DefaultParameters` list as a HTTP header. This will be added to each request made by the `RestClient`, and the `HttpClient` instance will not be modified. This is to allow the `HttpClient` instance to be reused for scenarios where different `User-Agent` headers are required. +- `Expect100Continue` + +The second option is to use a simple HTTP client factory as described [above](#simple-factory). + +Finally, you can use `IHttpClientFactory` that properly manages the client message handler lifecycle. Read more [here](./di.md). + +## Blazor support + +Inside a Blazor webassembly app, you can make requests to external API endpoints. Microsoft examples show how to do it with `HttpClient`, and it's also possible to use RestSharp for the same purpose. + +You need to remember that webassembly has some platform-specific limitations. Therefore, you won't be able to instantiate `RestClient` using all of its constructors. In fact, you can only use `RestClient` constructors that accept `HttpClient` or `HttpMessageHandler` as an argument. If you use the default parameterless constructor, it will call the option-based constructor with default options. The options-based constructor will attempt to create an `HttpMessageHandler` instance using the options provided, and it will fail with Blazor, as some of those options throw thw "Unsupported platform" exception. + +Here is an example how to register the `RestClient` instance globally as a singleton: + +```csharp +builder.Services.AddSingleton(new RestClient(new HttpClient())); +``` + +Then, on a page you can inject the instance: + +```html +@page "/fetchdata" +@using RestSharp +@inject RestClient _restClient +``` + +And then use it: + +```csharp +@code { + private WeatherForecast[]? forecasts; + + protected override async Task OnInitializedAsync() { + forecasts = await _restClient.GetJsonAsync("http://localhost:5104/weather"); + } + + public class WeatherForecast { + public DateTime Date { get; set; } + public int TemperatureC { get; set; } + public string? Summary { get; set; } + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + } +} +``` + +In this case, the call will be made to a WebAPI server hosted at `http://localhost:5104/weather`. Remember that if the WebAPI server is not hosting the webassembly itself, it needs to have a CORS policy configured to allow the webassembly origin to access the API endpoint from the browser. diff --git a/docs/versioned_docs/version-v113/usage/di.md b/docs/versioned_docs/version-v113/usage/di.md new file mode 100644 index 000000000..a6e8e77ae --- /dev/null +++ b/docs/versioned_docs/version-v113/usage/di.md @@ -0,0 +1,99 @@ +--- +sidebar_position: 7 +title: Dependency Injection +--- + +RestSharp integrates with the Microsoft [Dependency Injection](ttps://learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection) container and `IHttpClientFactory` via a separate package `RestSharp.Extensions.DependencyInjection`. + +You can register RestClient in the container as a transient dependency, and it will use `IHttpClientFactory` via `IRestClientFactory` for creating HTTP clients and message handlers that are managed by the factory. This approach offers the following benefits: + +* Provides a central location for naming and configuring logical RestClient instances. For example, a client named `github` could be registered and configured to access GitHub. A default client can be registered for general access. +* Manages the pooling and lifetime of underlying `HttpClientMessageHandler` instances. Automatic management avoids common DNS problems that occur when manually managing RestClient lifetime. + +Examples below are for a basic ASP.NET Core bootstrap with `WebApplicationBuilder`. + +## Default client + +Register `IHttpClientFactory`, `IRestClientFactory`, and `IRestClient` by calling `AddRestClient` in `Program.cs`: + +```csharp +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +builder.Services.AddRestClient(); +``` + +Then, you can get `IRestClient` injected to your services: + +```csharp +public class GetReposModel(IRestClient client) : PageModel { + public IEnumerable? GitHubBranches { get; set; } + + public async Task OnGet() { + var request = new RestRequest("https://api.github.com/repos/RestSharp/RestSharp/branches"); + var response = await client.ExecuteGetAsync>(request); + + if (response.IsSuccessful) { + GitHubBranches = response.Data; + } + } +} +``` + +It's possible to provide client options when registering the default client using one of the extensions: + +```csharp +// Specify the base URL +builder.Services.AddRestClient(new Uri("https://api.github.com")); + +// Provide custom options +var options = new RestClientOptions("https://api.github.com") { + Timeout = TimeSpan.FromSeconds(5); +} +builder.Services.AddRestClient(options); +``` + +## Named client + +Named clients are needed when you need multiple clients to be configured differently, which are then used in different parts of the application. For example, you need one client to call the GitHub API, and another client to call the Twilio API. Registering clients using meaningful names allows to retrieve them from `IRestClientFactory`. + +For example, a GitHub-specific client can be registered with `github` name: + +```csharp +var options = new RestClientOptions("https://api.github.com") { + Timeout = TimeSpan.FromSeconds(5); +} +builder.Services.AddRestClient("github", options); +``` + +Then, an application component can use the client factory to get the client instance: + +```csharp +public class GetReposModel(IRestClientFactory factory) : PageModel { + public IEnumerable? GitHubBranches { get; set; } + + public async Task OnGet() { + var request = new RestRequest("/repos/RestSharp/RestSharp/branches"); + var client = factory.CreateClient("github"); + var response = await client.ExecuteGetAsync>(request); + + if (response.IsSuccessful) { + GitHubBranches = response.Data; + } + } +} +``` + +As the default client, named clients can be registered with a base URL and with custom options. The most versatile extension allows configuring anything at named client registration: + +```csharp +builder.Services.AddRestClient( + "my-client", + options => { + options.BaseUrl = new Uri("https://api.github.com"); + options.Timeout = TimeSpan.FromSeconds(1); + options.Authenticator = new GitHubAuthenticator(builder.Configuration["GitHub:ApiToken"]); + }, + serialization => serialization.UseNewtonsoftJson() +); +``` \ No newline at end of file diff --git a/docs/versioned_docs/version-v113/usage/example.md b/docs/versioned_docs/version-v113/usage/example.md new file mode 100644 index 000000000..6182d8ff3 --- /dev/null +++ b/docs/versioned_docs/version-v113/usage/example.md @@ -0,0 +1,152 @@ +--- +sidebar_position: 1 +--- + +# Example + +RestSharp works best as the foundation for a proxy class for your API. Each API would most probably require different settings for `RestClient`. Hence, a dedicated API class (and its interface) gives you sound isolation between different `RestClient` instances and make them testable. + +For example, let's look at a simple Twitter API v2 client, which uses OAuth2 machine-to-machine authentication. For it to work, you would need to have access to the Twitter Developers portal, a project, and an approved application inside the project with OAuth2 enabled. + +## Client model + +Before implementing an API client, we need to have a model for it. The model includes an abstraction for the client, which has functions for the API calls we are interested to implement. In addition, the client model would include the necessary request and response models. Usually those are simple classes or records without logic, which are often referred to as DTOs (data transfer objects). + +This example starts with a single function that retrieves one Twitter user. Lets being by defining the API client interface: + +```csharp +public interface ITwitterClient { + Task GetUser(string user); +} +``` + +As the function returns a `TwitterUser` instance, we need to define it as a model: + +```csharp +public record TwitterUser(string Id, string Name, string Username); +``` + +## Client implementation + +When that is done, we can implement the interface and add all the necessary code blocks to get a working API client. + +The client class needs the following: +- A constructor for passing API credentials +- A wrapped `RestClient` instance with the Twitter API base URI pre-configured +- An authenticator to support authorizing the client using Twitter OAuth2 authentication +- The actual function to get the user (to implement the `ITwitterClient` interface) + +Creating an authenticator is described [below](#authenticator). + +Here's how the client implementation could look like: + +```csharp +public class TwitterClient : ITwitterClient, IDisposable { + readonly RestClient _client; + + public TwitterClient(string apiKey, string apiKeySecret) { + var options = new RestClientOptions("https://api.twitter.com/2"); + _client = new RestClient(options); + } + + public async Task GetUser(string user) { + var response = await _client.GetAsync>( + "users/by/username/{user}", + new { user } + ); + return response!.Data; + } + + record TwitterSingleObject(T Data); + + public void Dispose() { + _client?.Dispose(); + GC.SuppressFinalize(this); + } +} +``` + +It is also possible to use ASP.NET Core Options for configuring the client, instead of passing the credentials as strings. For example, we can add a class for Twitter client options, and use it in a constructor: + +```csharp +public class TwitterClientOptions(string ApiKey, string ApiSecret); + +public TwitterClient(IOptions options) { + var opt = new RestClientOptions("https://api.twitter.com/2"); + _client = new RestClient(options); +} +``` + +Then, you can register and configure the client using ASP.NET Core dependency injection container. + +Right now, the client won't really work as Twitter API requires authentication. It's covered in the next section. + +## Authenticator + +Before we can call the API itself, we need to get a bearer token. Twitter exposes an endpoint `https://api.twitter.com/oauth2/token`. As it follows the OAuth2 conventions, the code can be used to create an authenticator for some other vendors. + +First, we need a model for deserializing the token endpoint response. OAuth2 uses snake case for property naming, so we need to decorate model properties with `JsonPropertyName` attribute: + +```csharp +record TokenResponse { + [JsonPropertyName("token_type")] + public string TokenType { get; init; } + [JsonPropertyName("access_token")] + public string AccessToken { get; init; } +} +``` + +Next, we create the authenticator itself. It needs the API key and API key secret to call the token endpoint using basic HTTP authentication. In addition, we can extend the list of parameters with the base URL to convert it to a more generic OAuth2 authenticator. + +The easiest way to create an authenticator is to inherit from the `AuthenticatorBase` base class: + +```csharp +public class TwitterAuthenticator : AuthenticatorBase { + readonly string _baseUrl; + readonly string _clientId; + readonly string _clientSecret; + + public TwitterAuthenticator(string baseUrl, string clientId, string clientSecret) : base("") { + _baseUrl = baseUrl; + _clientId = clientId; + _clientSecret = clientSecret; + } + + protected override async ValueTask GetAuthenticationParameter(string accessToken) { + Token = string.IsNullOrEmpty(Token) ? await GetToken() : Token; + return new HeaderParameter(KnownHeaders.Authorization, Token); + } +} +``` + +During the first call made by the client using the authenticator, it will find out that the `Token` property is empty. It will then call the `GetToken` function to get the token once and reuse the token going forward. + +Now, we need to implement the `GetToken` function in the class: + +```csharp +async Task GetToken() { + var options = new RestClientOptions(_baseUrl){ + Authenticator = new HttpBasicAuthenticator(_clientId, _clientSecret), + }; + using var client = new RestClient(options); + + var request = new RestRequest("oauth2/token") + .AddParameter("grant_type", "client_credentials"); + var response = await client.PostAsync(request); + return $"{response!.TokenType} {response!.AccessToken}"; +} +``` + +As we need to make a call to the token endpoint, we need our own short-lived instance of `RestClient`. Unlike the actual Twitter client, it will use the `HttpBasicAuthenticator` to send the API key and secret as the username and password. The client then gets disposed as we only use it once. + +Here we add a POST parameter `grant_type` with `client_credentials` as its value. At the moment, it's the only supported value. + +The POST request will use the `application/x-www-form-urlencoded` content type by default. + +::: note +Sample code provided on this page is a production code. For example, the authenticator might produce undesired side effect when multiple requests are made at the same time when the token hasn't been obtained yet. It can be solved rather than simply using semaphores or synchronized invocation. +::: + +## Final words + +This page demonstrates how an API client can be implemented as a typed, configurable client with its own interface. Usage of the client in applications is not covered here as different application types and target frameworks have their own idiomatic ways to use HTTP clients. \ No newline at end of file diff --git a/docs/versioned_docs/version-v113/usage/execute.md b/docs/versioned_docs/version-v113/usage/execute.md new file mode 100644 index 000000000..36343f793 --- /dev/null +++ b/docs/versioned_docs/version-v113/usage/execute.md @@ -0,0 +1,172 @@ +--- +sidebar_position: 5 +title: Making calls +--- + +## Executing requests + +Once you've added all the parameters to your `RestRequest`, you are ready to make a request. + +`RestClient` has a single function for this: + +```csharp +public async Task ExecuteAsync( + RestRequest request, + CancellationToken cancellationToken = default +) +``` + +You can also avoid setting the request method upfront and use one of the overloads: + +```csharp +Task ExecuteGetAsync(RestRequest request, CancellationToken cancellationToken) +Task ExecutePostAsync(RestRequest request, CancellationToken cancellationToken) +Task ExecutePutAsync(RestRequest request, CancellationToken cancellationToken) +Task ExecuteDeleteAsync(RestRequest request, CancellationToken cancellationToken) +Task ExecuteHeadAsync(RestRequest request, CancellationToken cancellationToken) +Task ExecuteOptionsAsync(RestRequest request, CancellationToken cancellationToken) +``` + +When using any of those methods, you will get the response content as string in `response.Content`. + +RestSharp can deserialize the response for you. To use that feature, use one of the generic overloads: + +```csharp +Task> ExecuteAsync(RestRequest request, CancellationToken cancellationToken) +Task> ExecuteGetAsync(RestRequest request, CancellationToken cancellationToken) +Task> ExecutePostAsync(RestRequest request, CancellationToken cancellationToken) +Task> ExecutePutAsync(RestRequest request, CancellationToken cancellationToken) +Task> ExecuteDeleteAsync(RestRequest request, CancellationToken cancellationToken) +Task> ExecuteHeadAsync(RestRequest request, CancellationToken cancellationToken) +Task> ExecuteOptionsAsync(RestRequest request, CancellationToken cancellationToken) +``` + +:::note Beware of errors +All the overloads with names starting with `Execute` don't throw an exception if the server returns an error. Read more about it [here](../advanced/error-handling.md). +It allows you to inspect responses and handle remote server errors gracefully. Overloads without `Execute` prefix throw exceptions in case of any error, so you'd need to ensure to handle exceptions properly. +::: + +If you just need a deserialized response, you can use one of the extensions: + +```csharp +Task GetAsync(RestRequest request, CancellationToken cancellationToken) +Task PostAsync(RestRequest request, CancellationToken cancellationToken) +Task PutAsync(RestRequest request, CancellationToken cancellationToken) +Task PatchAsync(RestRequest request, CancellationToken cancellationToken) +Task DeleteAsync(RestRequest request, CancellationToken cancellationToken) +Task HeadAsync(RestRequest request, CancellationToken cancellationToken) +Task OptionsAsync(RestRequest request, CancellationToken cancellationToken) +``` + +Those extensions will throw an exception if the server returns an error, as there's no other way to float the error back to the caller. + +The `IRestClient` interface also has extensions for making requests without deserialization, which throw an exception if the server returns an error even if the client is configured to not throw exceptions. + +```csharp +Task GetAsync(RestRequest request, CancellationToken cancellationToken) +Task PostAsync(RestRequest request, CancellationToken cancellationToken) +Task PutAsync(RestRequest request, CancellationToken cancellationToken) +Task PatchAsync(RestRequest request, CancellationToken cancellationToken) +Task DeleteAsync(RestRequest request, CancellationToken cancellationToken) +Task HeadAsync(RestRequest request, CancellationToken cancellationToken) +Task OptionsAsync(RestRequest request, CancellationToken cancellationToken) +``` + +### Sync calls + +The preferred way for making requests is to execute them asynchronously as HTTP calls are IO-bound operations. +If you are unable to make async calls, all the functions about have sync overloads, which have the same names without `Async` suffix. +For example, for making a sync `GET` call you can use `ExecuteGet(request)` or `Get`, etc. + +## Requests without body + +Some HTTP methods don't suppose to be used with request body. For those methods, RestSharp supports making simplified calls without using `RestRequest`. All you need is to provide the resource path as a string. + +For example, you can make a `DELETE` call like this: + +```csharp +var response = await client.ExecuteDeleteAsync($"order/delete/{orderId}", cancellationToken); +``` + +Similarly, you can make `GET` calls with or without deserialization of the response using `ExecuteGetAsync(resource)`, `GetAsync(resource)`, `ExecuteGetAsync(resource)`, and `GetAsync(resource)` (see below). + +## JSON requests + +RestSharp provides an easier API for making calls to endpoints that accept and return JSON. + +### GET calls + +To make a simple `GET` call and get a deserialized JSON response with a pre-formed resource string, use this: + +```csharp +var response = await client.GetAsync("endpoint?foo=bar", cancellationToken); +``` + +:::note +In v111, `GetJsonAsync` is renamed to `GetAsync`. +::: + +You can also use a more advanced extension that uses an object to compose the resource string: + +```csharp +var client = new RestClient("https://example.org"); +var args = new { + id = "123", + foo = "bar" +}; +// Will make a call to https://example.org/endpoint/123?foo=bar +var response = await client.GetAsync("endpoint/{id}", args, cancellationToken); +``` + +It will search for the URL segment parameters matching any of the object properties and replace them with values. All the other properties will be used as query parameters. + +One note about `GetAsync` is that it will deserialize the response with any supported content type, not only JSON. + +### POST calls + +Similar things are available for `POST` requests. + +```csharp +var request = new CreateOrder("123", "foo", 10100); +// Will post the request object as JSON to "orders" and returns a +// JSON response deserialized to OrderCreated +var result = client.PostJsonAsync("orders", request, cancellationToken); +``` + +```csharp +var request = new CreateOrder("123", "foo", 10100); +// Will post the request object as JSON to "orders" and returns a +// status code, not expecting any response body +var statusCode = client.PostJsonAsync("orders", request, cancellationToken); +``` + +The same two extensions also exist for `PUT` requests (`PutJsonAsync`); + +## Downloading binary data + +There are two functions that allow you to download binary data from the remote API. + +First, there's `DownloadDataAsync`, which returns `Task`. This function allows you to open a stream reader and asynchronously stream large responses to memory or disk. + +## JSON streaming + +For HTTP API endpoints that stream the response data (like [Twitter search stream](https://developer.twitter.com/en/docs/twitter-api/tweets/filtered-stream/api-reference/get-tweets-search-stream)) you can use RestSharp with `StreamJsonAsync`, which returns an `IAsyncEnumerable`: + +```csharp +public async IAsyncEnumerable SearchStream( + [EnumeratorCancellation] CancellationToken cancellationToken = default +) { + var response = _client.StreamJsonAsync>( + "tweets/search/stream", cancellationToken + ); + + await foreach (var item in response.WithCancellation(cancellationToken)) { + yield return item.Data; + } +} +``` + +The main limitation of this function is that it expects each JSON object to be returned as a single line. It is unable to parse the response by combining multiple lines into a JSON string. + diff --git a/docs/versioned_docs/version-v113/usage/request.md b/docs/versioned_docs/version-v113/usage/request.md new file mode 100644 index 000000000..61de32de5 --- /dev/null +++ b/docs/versioned_docs/version-v113/usage/request.md @@ -0,0 +1,350 @@ +--- +sidebar_position: 4 +title: Preparing requests +--- + +## Create a request + +Before making a request using `RestClient`, you need to create a request instance: + +```csharp +var request = new RestRequest(resource); // resource is the sub-path of the client base path +``` + +The default request type is `GET` and you can override it by setting the `Method` property. You can also set the method using the constructor overload: + +```csharp +var request = new RestRequest(resource, Method.Post); +``` + +After you've created a `RestRequest`, you can add parameters to it. Below, you can find all the parameter types supported by RestSharp. + +## Request headers + +Adds the header parameter as an HTTP header that is sent along with the request. The header name is the parameter's name and the header value is the value. + +You can use one of the following request methods to add a header parameter: + +```csharp +AddHeader(string name, string value); +AddHeader(string name, T value); // value will be converted to string +AddOrUpdateHeader(string name, string value); // replaces the header if it already exists +``` + +For example: + +```csharp +var request = new RestRequest("/path").AddHeader("X-Key", someKey); +``` + +You can also add header parameters to the client, and they will be added to every request made by the client. This is useful for adding authentication headers, for example. + +```csharp +client.AddDefaultHeader(string name, string value); +``` + +:::warning Avoid setting Content-Type header +RestSharp will use the correct content type by default. Avoid adding the `Content-Type` header manually to your requests unless you are absolutely sure it is required. You can add a custom content type to the [body parameter](#request-body) itself. +::: + +## Get or Post parameters + +The default RestSharp parameter type is `GetOrPostParameter`. You can add `GetOrPost` parameter to the request using the `AddParameter` function: + +```csharp +request + .AddParameter("name1", "value1") + .AddParameter("name2", "value2"); +``` + +`GetOrPost` behaves differently based on the HTTP method. If you execute a `GET` call, RestSharp will append the parameters to the URL in the form `url?name1=value1&name2=value2`. + +On a `POST` or `PUT` requests, it depends on whether you have files attached to a request. +If not, the parameters will be sent as the body of the request in the form `name1=value1&name2=value2`. Also, the request will be sent as `application/x-www-form-urlencoded`. + +In both cases, name and value will automatically be URL-encoded, unless specified otherwise: + +```csharp +request.AddParameter("name", "Væ üé", false); // don't encode the value +``` + +If you have files, RestSharp will send a `multipart/form-data` request. Your parameters will be part of this request in the form: + +``` +Content-Type: text/plain; charset=utf-8 +Content-Disposition: form-data; name="parameterName" + +ParameterValue +``` + +Sometimes, you need to override the default content type for the parameter when making a multipart form call. It's possible to do by setting the `ContentType` property of the parameter object. As an example, the code below will create a POST parameter with JSON value, and set the appropriate content type: + +```csharp +var parameter = new GetOrPostParameter("someJson", "{\"attributeFormat\":\"pdf\"}") { + ContentType = "application/json" +}; +request.AddParameter(parameter); +``` + +When the request is set to use multipart content, the parameter will be sent as part of the request with the specified content type: + +``` +Content-Type: application/json; charset=utf-8 +Content-Disposition: form-data; name="someJson" + +{"attributeFormat":"pdf"} +``` + +You can also add `GetOrPost` parameter as a default parameter to the client. This will add the parameter to every request made by the client. + +```csharp +client.AddDefaultParameter("foo", "bar"); +``` + +It will work the same way as request parameters, except that it will be added to every request. + +## Query string + +`QueryString` works like `GetOrPost`, except that it always appends the parameters to the url in the form `url?name1=value1&name2=value2`, regardless of the request method. + +Example: + +```csharp +var client = new RestClient("https://search.me"); +var request = new RestRequest("search") + .AddParameter("foo", "bar"); +var response = await client.GetAsync(request); +``` + +It will send a `GET` request to `https://search.me/search?foo=bar`. + +For `POST`-style requests you need to add the query string parameter explicitly: + +```csharp +request.AddQueryParameter("foo", "bar"); +``` + +In some cases, you might need to prevent RestSharp from encoding the query string parameter. +To do so, set the `encode` argument to `false` when adding the parameter: + +```csharp +request.AddQueryParameter("foo", "bar/fox", false); +``` + +You can also add a query string parameter as a default parameter to the client. This will add the parameter to every request made by the client. + +```csharp +client.AddDefaultQueryParameter("foo", "bar"); +``` + +The line above will result in all the requests made by that client instance to have `foo=bar` in the query string for all the requests made by that client. + +## Using AddObject + +You can avoid calling `AddParameter` multiple times if you collect all the parameters in an object, and then use `AddObject`. +For example, this code: + +```csharp +var params = new { + status = 1, + priority = "high", + ids = new [] { "123", "456" } +}; +request.AddObject(params); +``` + +is equivalent to: + +```csharp +request.AddParameter("status", 1); +request.AddParameter("priority", "high"); +request.AddParameter("ids", "123,456"); +``` + +Remember that `AddObject` only works if your properties have primitive types. It also works with collections of primitive types as shown above. + +If you need to override the property name or format, you can do it using the `RequestProperty` attribute. For example: + +```csharp +public class RequestModel { + // override the name and the format + [RequestProperty(Name = "from_date", Format = "d")] + public DateTime FromDate { get; set; } +} + +// add it to the request +request.AddObject(new RequestModel { FromDate = DateTime.Now }); +``` + +In this case, the request will get a GET or POST parameter named `from_date` and its value would be the current date in short date format. + +## Using AddObjectStatic + +Request function `AddObjectStatic(...)` allows using pre-compiled expressions for getting property values. Compared to `AddObject` that uses reflections for each call, `AddObjectStatic` caches functions to retrieve properties from an object of type `T`, so it works much faster. + +You can instruct `AddObjectStatic` to use custom parameter names and formats, as well as supply the list of properties than need to be used as parameters. The last option could be useful if the type `T` has properties that don't need to be sent with HTTP call. + +To use custom parameter name or format, use the `RequestProperty` attribute. For example: + +```csharp +class TestObject { + [RequestProperty(Name = "some_data")] + public string SomeData { get; set; } + + [RequestProperty(Format = "d")] + public DateTime SomeDate { get; set; } + + [RequestProperty(Name = "dates", Format = "d")] + public DateTime[] DatesArray { get; set; } + + public int Plain { get; set; } + public DateTime[] PlainArray { get; set; } +} +``` + +## URL segment parameter + +Unlike `GetOrPost`, URL segment parameter replaces placeholder values in the request URL: + +```csharp +var request = new RestRequest("health/{entity}/status") + .AddUrlSegment("entity", "s2"); +``` + +When the request executes, RestSharp will try to match any `{placeholder}` with a parameter of that name (without the `{}`) and replace it with the value. So the above code results in `health/s2/status` being the URL. + +You can also add `UrlSegment` parameter as a default parameter to the client. This will add the parameter to every request made by the client. + +```csharp +client.AddDefaultUrlSegment("foo", "bar"); +``` + +## Cookies + +You can add cookies to a request using the `AddCookie` method: + +```csharp +request.AddCookie("foo", "bar"); +``` + +RestSharp will add cookies from the request as cookie headers and then extract the matching cookies from the response. You can observe and extract response cookies using the `RestResponse.Cookies` properties, which has the `CookieCollection` type. + +However, the usage of a default URL segment parameter is questionable as you can just include the parameter value to the base URL of the client. There is, however, a `CookieContainer` instance on the request level. You can either assign the pre-populated container to `request.CookieContainer`, or let the container be created by the request when you call `AddCookie`. Still, the container is only used to extract all the cookies from it and create cookie headers for the request instead of using the container directly. It's because the cookie container is normally configured on the `HttpClientHandler` level and cookies are shared between requests made by the same client. In most of the cases this behaviour can be harmful. + +If your use case requires sharing cookies between requests made by the client instance, you can use the client-level `CookieContainer`, which you must provide as the options' property. You can add cookies to the container using the container API. No response cookies, however, would be auto-added to the container, but you can do it in code by getting cookies from the `Cookes` property of the response and adding them to the client-level container available via `IRestClient.Options.CookieContainer` property. + +## Request Body + +RestSharp supports multiple ways to add a request body: +- `AddJsonBody` for JSON payloads +- `AddXmlBody` for XML payloads +- `AddStringBody` for pre-serialized payloads + +We recommend using `AddJsonBody` or `AddXmlBody` methods instead of `AddParameter` with type `BodyParameter`. Those methods will set the proper request type and do the serialization work for you. + +When you make a `POST`, `PUT` or `PATCH` request and added `GetOrPost` [parameters](#get-or-post-parameters), RestSharp will send them as a URL-encoded form request body by default. When a request also has files, it will send a `multipart/form-data` request. You can also instruct RestSharp to send the body as `multipart/form-data` by setting the `AlwaysMultipartFormData` property to `true`. + +You can specify a custom body content type if necessary. The `contentType` argument is available in all the overloads that add a request body. + +It is not possible to add client-level default body parameters. + +### String body + +If you have a pre-serialized payload like a JSON string, you can use `AddStringBody` to add it as a body parameter. You need to specify the content type, so the remote endpoint knows what to do with the request body. For example: + +```csharp +const json = "{ \"data\": { \"foo\": \"bar\" } }"; +request.AddStringBody(json, ContentType.Json); +``` + +### JSON body + +When you call `AddJsonBody`, it does the following for you: + +- Instructs the RestClient to serialize the object parameter as JSON when making a request +- Sets the content type to `application/json` +- Sets the internal data type of the request body to `DataType.Json` + +Here is the example: + +```csharp +var param = new MyClass { IntData = 1, StringData = "test123" }; +request.AddJsonBody(param); +``` + +It is possible to override the default content type by supplying the `contentType` argument. For example: + +```csharp +request.AddJsonBody(param, "text/x-json"); +``` + +If you use a pre-serialized string with `AddJsonBody`, it will be sent as-is. The `AddJsonBody` will detect if the parameter is a string and will add it as a string body with JSON content type. +Essentially, it means that top-level strings won't be serialized as JSON when you use `AddJsonBody`. To overcome this issue, you can use an overload of `AddJsonBody`, which allows you to tell RestSharp to serialize the string as JSON: + +```csharp +const string payload = @" +""requestBody"": { + ""content"": { + ""application/json"": { + ""schema"": { + ""type"": ""string"" + } + } + } +},"; +request.AddJsonBody(payload, forceSerialize: true); // the string will be serialized +request.AddJsonBody(payload); // the string will NOT be serialized and will be sent as-is +``` + +### XML body + +When you call `AddXmlBody`, it does the following for you: + +- Instructs the RestClient to serialize the object parameter as XML when making a request +- Sets the content type to `application/xml` +- Sets the internal data type of the request body to `DataType.Xml` + +:::warning +Do not send XML string to `AddXmlBody`; it won't work! +::: + +## Uploading files + +To add a file to the request you can use the `RestRequest` function called `AddFile`. The main function accepts the `FileParameter` argument: + +```csharp +request.AddFile(fileParameter); +``` + +You can instantiate the file parameter using `FileParameter.Create` that accepts a bytes array, or `FileParameter.FromFile`, which will load the file from disk. + +There are also extension functions that wrap the creation of `FileParameter` inside: + +```csharp +// Adds a file from disk +AddFile(parameterName, filePath, contentType); + +// Adds an array of bytes +AddFile(parameterName, bytes, fileName, contentType); + +// Adds a stream returned by the getFile function +AddFile(parameterName, getFile, fileName, contentType); +``` + +Remember that `AddFile` will set all the necessary headers, so please don't try to set content headers manually. + +You can also provide file upload options to the `AddFile` call. The options are: +- `DisableFilenameEncoding` (default `false`): if set to `true`, RestSharp will not encode the file name in the `Content-Disposition` header +- `DisableFilenameStar` (default `true`): if set to `true`, RestSharp will not add the `filename*` parameter to the `Content-Disposition` header + +Example of using the options: + +```csharp +var options = new FileParameterOptions { + DisableFilenameEncoding = true, + DisableFilenameStar = false +}; +request.AddFile("file", filePath, options: options); +``` + +The options specified in the snippet above usually help when you upload files with non-ASCII characters in their names. diff --git a/docs/versioned_docs/version-v113/usage/response.md b/docs/versioned_docs/version-v113/usage/response.md new file mode 100644 index 000000000..dbaf302b4 --- /dev/null +++ b/docs/versioned_docs/version-v113/usage/response.md @@ -0,0 +1,36 @@ +--- +sidebar_position: 6 +title: Handling responses +--- + +All `Execute{Method}Async` functions return an instance of `RestResponse`. Similarly, `Execute{Method}Async` return a generic instance of `RestResponse` where `T` is the response object type. + +Response object contains the following properties: + +| Property | Type | Description | +|--------------------------|-----------------------------------------------------|------------------------------------------------------------------------------------------| +| `Request` | `RestRequest` | Request instance that was used to get the response. | +| `ContentType` | `string?` | Response content type. `Null` if response has no content. | +| `ContentLength` | `long?` | Response content length. `Null` if response has no content. | +| `ContentEncoding` | `ICollection` | Content encoding collection. Empty if response has no content. | +| `Content` | `string?` | Response content as string. `Null` if response has no content. | +| `IsSuccessfulStatusCode` | `bool` | Indicates if response was successful, so no errors were reported by the server. | +| `ResponseStatus` | `None`, `Completed`, `Error`, `TimedOut`, `Aborted` | Response completion status. Note that completed responses might still return errors. | +| `IsSuccessful` | `bool` | `True` when `IsSuccessfulStatusCode` is `true` and `ResponseStatus` is `Completed`. | +| `StatusDescription` | `string?` | Response status description, if available. | +| `RawBytes` | `byte[]?` | Response content as byte array. `Null` if response has no content. | +| `ResponseUri` | `Uri?` | URI of the response, which might be different from request URI in case of redirects. | +| `Server` | `string?` | Server header value of the response. | +| `Cookies` | `CookieCollection?` | Collection of cookies received with the response, if any. | +| `Headers` | Collection of `HeaderParameter` | Response headers. | +| `ContentHeaders` | Collection of `HeaderParameter` | Response content headers. | +| `ErrorMessage` | `string?` | Transport or another non-HTTP error generated while attempting request. | +| `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. | + +In addition, `RestResponse` has one additional property: + +| Property | Type | Description | +|----------|------|-----------------------------------------------------------------------------------------------------------------------------------------------------------| +| `Data` | `T?` | Deserialized response object. `Null` if there's no content in the response, deserializer failed to understand the response content, or if request failed. | diff --git a/docs/versioned_sidebars/version-v110-sidebars.json b/docs/versioned_sidebars/version-v110-sidebars.json new file mode 100644 index 000000000..caea0c03b --- /dev/null +++ b/docs/versioned_sidebars/version-v110-sidebars.json @@ -0,0 +1,8 @@ +{ + "tutorialSidebar": [ + { + "type": "autogenerated", + "dirName": "." + } + ] +} diff --git a/docs/versioned_sidebars/version-v111-sidebars.json b/docs/versioned_sidebars/version-v111-sidebars.json new file mode 100644 index 000000000..caea0c03b --- /dev/null +++ b/docs/versioned_sidebars/version-v111-sidebars.json @@ -0,0 +1,8 @@ +{ + "tutorialSidebar": [ + { + "type": "autogenerated", + "dirName": "." + } + ] +} diff --git a/docs/versioned_sidebars/version-v112-sidebars.json b/docs/versioned_sidebars/version-v112-sidebars.json new file mode 100644 index 000000000..caea0c03b --- /dev/null +++ b/docs/versioned_sidebars/version-v112-sidebars.json @@ -0,0 +1,8 @@ +{ + "tutorialSidebar": [ + { + "type": "autogenerated", + "dirName": "." + } + ] +} diff --git a/docs/versioned_sidebars/version-v113-sidebars.json b/docs/versioned_sidebars/version-v113-sidebars.json new file mode 100644 index 000000000..caea0c03b --- /dev/null +++ b/docs/versioned_sidebars/version-v113-sidebars.json @@ -0,0 +1,8 @@ +{ + "tutorialSidebar": [ + { + "type": "autogenerated", + "dirName": "." + } + ] +} diff --git a/docs/versions.json b/docs/versions.json new file mode 100644 index 000000000..53c8813a5 --- /dev/null +++ b/docs/versions.json @@ -0,0 +1,6 @@ +[ + "v113", + "v112", + "v111", + "v110" +] diff --git a/gen/SourceGenerator/Extensions.cs b/gen/SourceGenerator/Extensions.cs new file mode 100644 index 000000000..0e740eb6e --- /dev/null +++ b/gen/SourceGenerator/Extensions.cs @@ -0,0 +1,46 @@ +// 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. + +namespace SourceGenerator; + +static class Extensions { + extension(Compilation compilation) { + public IEnumerable FindClasses(Func predicate) + => compilation.SyntaxTrees + .Select(tree => compilation.GetSemanticModel(tree)) + .SelectMany(model => model.SyntaxTree.GetRoot().DescendantNodes().OfType()) + .Where(predicate); + + public IEnumerable FindAnnotatedClasses(string attributeName, bool strict) { + return compilation.FindClasses( + syntax => syntax.AttributeLists.Any(list => list.Attributes.Any(CheckAttribute)) + ); + + bool CheckAttribute(AttributeSyntax attr) { + var name = attr.Name.ToString(); + return strict ? name == attributeName : name.StartsWith(attributeName); + } + } + } + + public static IEnumerable GetBaseTypesAndThis(this ITypeSymbol type) { + var current = type; + + while (current != null) { + yield return current; + + current = current.BaseType; + } + } +} \ No newline at end of file diff --git a/gen/SourceGenerator/ImmutableGenerator.cs b/gen/SourceGenerator/ImmutableGenerator.cs new file mode 100644 index 000000000..e0860bd67 --- /dev/null +++ b/gen/SourceGenerator/ImmutableGenerator.cs @@ -0,0 +1,104 @@ +// 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. +// + +namespace SourceGenerator; + +[Generator(LanguageNames.CSharp)] +public class ImmutableGenerator : IIncrementalGenerator { + public void Initialize(IncrementalGeneratorInitializationContext context) { + var c = context.CompilationProvider.SelectMany((x, _) => GetImmutableClasses(x)); + + context.RegisterSourceOutput( + c.Collect(), + static (ctx, sources) => { + foreach (var source in sources) { + ctx.AddSource(source.Item1, source.Item2); + } + } + ); + return; + + IEnumerable<(string, SourceText)> GetImmutableClasses(Compilation compilation) { + var mutableClasses = compilation.FindAnnotatedClasses("GenerateImmutable", strict: true); + + foreach (var mutableClass in mutableClasses) { + var immutableClass = GenerateImmutableClass(mutableClass, compilation); + yield return ($"ReadOnly{mutableClass.Identifier.Text}.cs", SourceText.From(immutableClass, Encoding.UTF8)); + } + } + } + + static string GenerateImmutableClass(TypeDeclarationSyntax mutableClass, Compilation compilation) { + var containingNamespace = compilation.GetSemanticModel(mutableClass.SyntaxTree).GetDeclaredSymbol(mutableClass)!.ContainingNamespace; + var namespaceName = containingNamespace.ToDisplayString(); + var className = mutableClass.Identifier.Text; + var usings = mutableClass.SyntaxTree.GetCompilationUnitRoot().Usings.Select(u => u.ToString()); + + var properties = GetDefinitions(SyntaxKind.SetKeyword) + .Select( + prop => { + var xml = prop.GetLeadingTrivia().FirstOrDefault(x => x.IsKind(SyntaxKind.SingleLineDocumentationCommentTrivia)).GetStructure(); + return $"/// {xml} public {prop.Type} {prop.Identifier.Text} {{ get; }}"; + } + ) + .ToArray(); + + var props = GetDefinitions(SyntaxKind.SetKeyword).ToArray(); + + const string argName = "inner"; + + var mutableProperties = props.Select(prop => $" {prop.Identifier.Text} = {argName}.{prop.Identifier.Text};"); + + var constructor = $$""" + public ReadOnly{{className}}({{className}} {{argName}}) { + {{string.Join("\n", mutableProperties)}} + CopyAdditionalProperties({{argName}}); + } + """; + + const string template = """ + {Usings} + + namespace {Namespace}; + + public partial class ReadOnly{ClassName} { + {Constructor} + + partial void CopyAdditionalProperties({ClassName} {ArgName}); + + {Properties} + } + """; + + var code = template + .Replace("{Usings}", string.Join("\n", usings)) + .Replace("{Namespace}", namespaceName) + .Replace("{ClassName}", className) + .Replace("{Constructor}", constructor) + .Replace("{ArgName}", argName) + .Replace("{Properties}", string.Join("\n", properties)); + + return code; + + IEnumerable GetDefinitions(SyntaxKind kind) + => mutableClass.Members + .OfType() + .Where( + prop => + prop.AccessorList!.Accessors.Any(accessor => accessor.Keyword.IsKind(kind)) && + prop.AttributeLists.All(list => list.Attributes.All(attr => attr.Name.ToString() != "Exclude")) + ); + } +} \ No newline at end of file diff --git a/gen/SourceGenerator/InheritedCloneGenerator.cs b/gen/SourceGenerator/InheritedCloneGenerator.cs new file mode 100644 index 000000000..1e3843ff9 --- /dev/null +++ b/gen/SourceGenerator/InheritedCloneGenerator.cs @@ -0,0 +1,132 @@ +// 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. + +namespace SourceGenerator; + +[Generator(LanguageNames.CSharp)] +public class InheritedCloneGenerator : IIncrementalGenerator { + const string AttributeName = "GenerateClone"; + + public void Initialize(IncrementalGeneratorInitializationContext context) { + var c = context.CompilationProvider.SelectMany((x, _) => GetClones(x)); + + context.RegisterSourceOutput( + c.Collect(), + static (ctx, sources) => { + foreach (var source in sources) { + ctx.AddSource(source.Item1, source.Item2); + } + } + ); + return; + + IEnumerable<(string, SourceText)> GetClones(Compilation compilation) { + var candidates = compilation.FindAnnotatedClasses(AttributeName, false); + + foreach (var candidate in candidates) { + var semanticModel = compilation.GetSemanticModel(candidate.SyntaxTree); + var genericClassSymbol = semanticModel.GetDeclaredSymbol(candidate); + if (genericClassSymbol == null) continue; + + var attributeData = genericClassSymbol.GetAttributes().FirstOrDefault(a => a.AttributeClass?.Name == $"{AttributeName}Attribute"); + var methodName = (string)attributeData.NamedArguments.FirstOrDefault(arg => arg.Key == "Name").Value.Value; + var baseType = attributeData.NamedArguments.FirstOrDefault(arg => arg.Key == "BaseType").Value.Value; + var maybeMutate = attributeData.NamedArguments.FirstOrDefault(arg => arg.Key == "Mutate").Value.Value; + var mutate = maybeMutate as bool? ?? false; + + // Get the generic argument type where properties need to be copied from + var attributeSyntax = candidate.AttributeLists + .SelectMany(l => l.Attributes) + .FirstOrDefault(a => a.Name.ToString().StartsWith(AttributeName)); + if (attributeSyntax == null) continue; // This should never happen + + var code = GenerateMethod(candidate, genericClassSymbol, (INamedTypeSymbol)baseType, methodName, (bool)mutate!); + yield return ($"{genericClassSymbol.Name}.Clone.g.cs", SourceText.From(code, Encoding.UTF8)); + } + } + } + + static string GenerateMethod( + TypeDeclarationSyntax classToExtendSyntax, + INamedTypeSymbol classToExtendSymbol, + INamedTypeSymbol classToClone, + string methodName, + bool mutate + ) { + var namespaceName = classToExtendSymbol.ContainingNamespace.ToDisplayString(); + var className = classToExtendSyntax.Identifier.Text; + var genericTypeParameters = string.Join(", ", classToExtendSymbol.TypeParameters.Select(tp => tp.Name)); + var classDeclaration = classToExtendSymbol.TypeParameters.Length > 0 ? $"{className}<{genericTypeParameters}>" : className; + + var all = classToClone.GetBaseTypesAndThis(); + var props = all.SelectMany(x => x.GetMembers().OfType()).ToArray(); + var usings = classToExtendSyntax.SyntaxTree.GetCompilationUnitRoot().Usings.Select(u => u.ToString()); + + var constructorParams = classToExtendSymbol.Constructors.First().Parameters.ToArray(); + var constructorArgs = string.Join(", ", constructorParams.Select(p => $"original.{GetPropertyName(p.Name, props)}")); + var constructorParamNames = constructorParams.Select(p => p.Name).ToArray(); + + var endLine = mutate ? ";" : ","; + var spaces = string.Empty.PadRight(mutate ? 8 : 12); + var properties = props + // ReSharper disable once PossibleUnintendedLinearSearchInSet + .Where(prop => !constructorParamNames.Contains(prop.Name, StringComparer.OrdinalIgnoreCase) && prop.SetMethod != null) + .Select(prop => $"{spaces}{prop.Name} = original.{prop.Name}{endLine}") + .ToArray(); + + const string immutableTemplate = + """ + {Usings} + + namespace {Namespace}; + + public partial class {ClassDeclaration} { + public static {ClassDeclaration} {MethodName}({OriginalClassName} original) { + return new {ClassDeclaration}({ConstructorArgs}) { + {Properties} + }; + } + } + """; + const string mutableTemplate = + """ + {Usings} + + namespace {Namespace}; + + public partial class {ClassDeclaration} { + public void {MethodName}({OriginalClassName} original) { + {Properties} + } + } + """; + var template = mutate ? mutableTemplate : immutableTemplate; + + var code = template + .Replace("{Usings}", string.Join("\n", usings)) + .Replace("{Namespace}", namespaceName) + .Replace("{ClassDeclaration}", classDeclaration) + .Replace("{OriginalClassName}", classToClone.Name) + .Replace("{MethodName}", methodName) + .Replace("{ConstructorArgs}", constructorArgs) + .Replace("{Properties}", string.Join("\n", properties).TrimEnd(',')); + + return code; + + static string GetPropertyName(string parameterName, IPropertySymbol[] properties) { + var property = properties.FirstOrDefault(p => string.Equals(p.Name, parameterName, StringComparison.OrdinalIgnoreCase)); + return property?.Name ?? parameterName; + } + } +} \ No newline at end of file diff --git a/gen/SourceGenerator/Properties/launchSettings.json b/gen/SourceGenerator/Properties/launchSettings.json new file mode 100644 index 000000000..cd06df480 --- /dev/null +++ b/gen/SourceGenerator/Properties/launchSettings.json @@ -0,0 +1,9 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "Generators": { + "commandName": "DebugRoslynComponent", + "targetProject": "../../src/RestSharp/RestSharp.csproj" + } + } +} \ No newline at end of file diff --git a/gen/SourceGenerator/SourceGenerator.csproj b/gen/SourceGenerator/SourceGenerator.csproj new file mode 100644 index 000000000..2d26b1392 --- /dev/null +++ b/gen/SourceGenerator/SourceGenerator.csproj @@ -0,0 +1,26 @@ + + + netstandard2.0 + preview + enable + disable + false + true + false + + + + + + + + + + + + + + + + + diff --git a/package.cmd b/package.cmd deleted file mode 100644 index 41e87ed16..000000000 --- a/package.cmd +++ /dev/null @@ -1,49 +0,0 @@ -%windir%\Microsoft.NET\Framework\v4.0.30319\msbuild.exe RestSharp.sln /t:Clean,Rebuild /p:Configuration=Release /fileLogger - -if not exist Download\Net4 mkdir Download\Net4\ -if not exist Download\Silverlight mkdir Download\Silverlight\ -if not exist Download\WindowsPhone mkdir Download\WindowsPhone\ -if not exist Download\package\lib\net35 mkdir Download\package\lib\net35\ -if not exist Download\package\lib\net35-client mkdir Download\package\lib\net35-client\ -if not exist Download\package\lib\net4 mkdir Download\package\lib\net4\ -if not exist Download\package\lib\net4-client mkdir Download\package\lib\net4-client\ -if not exist Download\package\lib\sl4-wp71 mkdir Download\package\lib\sl4-wp71\ -if not exist Download\package\lib\sl4 mkdir Download\package\lib\sl4\ - -copy RestSharp\bin\Release\RestSharp.dll Download\ -copy RestSharp\bin\Release\RestSharp.xml Download\ - -copy RestSharp.Net4\bin\Release\RestSharp.dll Download\Net4\ -copy RestSharp.Net4\bin\Release\RestSharp.xml Download\Net4\ - -copy RestSharp.Silverlight\bin\Release\RestSharp.Silverlight.dll Download\Silverlight\ -copy RestSharp.Silverlight\bin\Release\RestSharp.Silverlight.xml Download\Silverlight\ - -copy RestSharp.WindowsPhone\bin\Release\RestSharp.WindowsPhone.dll Download\WindowsPhone\ -copy RestSharp.WindowsPhone\bin\Release\RestSharp.WindowsPhone.xml Download\WindowsPhone\ - -copy LICENSE.txt Download\ -copy readme.txt Download\ -copy readme.txt Download\package\ - -copy RestSharp\bin\Release\RestSharp.dll Download\Package\lib\net35\ -copy RestSharp\bin\Release\RestSharp.dll Download\Package\lib\net35-client\ - -copy RestSharp.Net4\bin\Release\RestSharp.dll Download\Package\lib\net4\ -copy RestSharp.Net4\bin\Release\RestSharp.dll Download\Package\lib\net4-client\ - -copy RestSharp.Silverlight\bin\Release\RestSharp.Silverlight.dll Download\Package\lib\sl4\ -copy RestSharp.WindowsPhone\bin\Release\RestSharp.WindowsPhone.dll Download\Package\lib\sl4-wp71\ - -copy RestSharp\bin\Release\RestSharp.xml Download\Package\lib\net35\ -copy RestSharp\bin\Release\RestSharp.xml Download\Package\lib\net35-client\ - -copy RestSharp.Net4\bin\Release\RestSharp.xml Download\Package\lib\net4\ -copy RestSharp.Net4\bin\Release\RestSharp.xml Download\Package\lib\net4-client\ - -copy RestSharp.Silverlight\bin\Release\RestSharp.Silverlight.xml Download\Package\lib\sl4\ -copy RestSharp.WindowsPhone\bin\Release\RestSharp.WindowsPhone.xml Download\Package\lib\sl4-wp71\ - -tools\nuget.exe update -self -tools\nuget.exe pack restsharp-computed.nuspec -BasePath Download\Package -Output Download -rm restsharp-computed.nuspec \ No newline at end of file diff --git a/props/Common.props b/props/Common.props new file mode 100644 index 000000000..f0ee2b686 --- /dev/null +++ b/props/Common.props @@ -0,0 +1,13 @@ + + + $([System.IO.Directory]::GetParent($(MSBuildThisFileDirectory)).Parent.FullName) + $(RepoRoot)\RestSharp.snk + true + preview + enable + enable + + + + + \ No newline at end of file diff --git a/readme.txt b/readme.txt deleted file mode 100644 index 44a1b2fe7..000000000 --- a/readme.txt +++ /dev/null @@ -1,22 +0,0 @@ -*** IMPORTANT CHANGE IN RESTSHARP VERSION 103 *** - -In 103.0, JSON.NET was removed as a dependency. - -If this is still installed in your project and no other libraries depend on -it you may remove it from your installed packages. - -There is one breaking change: the default Json*Serializer* is no longer -compatible with Json.NET. To use Json.NET for serialization, copy the code -from https://github.com/restsharp/RestSharp/blob/86b31f9adf049d7fb821de8279154f41a17b36f7/RestSharp/Serializers/JsonSerializer.cs -and register it with your client: - -var client = new RestClient(); -client.JsonSerializer = new YourCustomSerializer(); - -The default Json*Deserializer* is mostly compatible, but it does not support -all features which Json.NET has (like the ability to support a custom [JsonConverter] -by decorating a certain property with an attribute). If you need these features, you -must take care of the deserialization yourself to get it working. - -If you run into any compatibility issues with deserialization, -please report it to http://groups.google.com/group/restsharp diff --git a/releasenotes.markdown b/releasenotes.markdown deleted file mode 100644 index a2d423fa6..000000000 --- a/releasenotes.markdown +++ /dev/null @@ -1,115 +0,0 @@ -# RestSharp Release Notes - -## 104.3.3 - -To see all commits for this version, [click here](https://github.com/RestSharp/RestSharp/compare/104.2...104.3.3). - -### New Features/Improvements - -* Support for query string parameters on POST requests -* Deserialize an integer to a bool property -* Enable Task extensions for Monotouch and Monodroid platforms -* Support for deserializing a dictionary of lists - -### Bug Fixes - -* Fixed regression that prevented deserializing requests when non-protocol errors occurred -* Properly URL encode strings longer than 32766 characters - -## 104.2 - -To see all commits for this version, [click here](https://github.com/RestSharp/RestSharp/compare/104.1...104.2). - -### New Features - -* Allow specifying the body of a `PUT` or `POST` to be specified as a byte array. -* Added `ExecuteAsync` overloads that return `Task` -* Improved handling of nullable types -* Support `DateTimeOffset` to `XmlDeserializer` - -### Bug Fixes - -* Crash if an XML attribute contains empty string -* Adding array of int to request -* Support XAuth parameters for OAuth parameter handling -* Memory leak around handling of Accepts header -* `ConfigureProxy` was not being called for async request -* Serialization for classes with `IList` properties -* Exception when executing async requests on multiple threads with one `RestClient` -* ResponseStatus.Aborted was not being set if request was aborted -* ClientCertificate threw `NotImplementedException` on Mono -* Fix decimal parsing for small decimal values - -## 104.1 - -* Fixed bug where ExecuteAsync sometimes doesn't send data - -## 104.0 - -* Fixed Windows Phone and Silverlight to use culture when calling Convert.ChangeType() (thanks trydis) -* Added support for non-standard HTTP methods (thanks jhoerr) - New API methods include: - * `IRestClient.ExecuteAsyncGet()` - * `IRestClient.ExecuteAsyncPost()` - * `IRestClient.ExecuteAsyncGet()` - * `IRestClient.ExecuteAsyncPost()` - - See [groups discussion](https://groups.google.com/forum/?fromgroups=#!topic/restsharp/FCLGE5By7AU) for more info - -* Resolved an xAuth support issue in the OAuth1Authenticator (thanks artema) -* Change AddDefaultParameter methods to be extension methods (thanks haacked) - Added `RestClientExtensions.AddDefaultParameter()` with 4 overloads. See pull request [#311](https://github.com/restsharp/RestSharp/pull/311) for more info - -* Adding support for deserializing enums from integer representations (thanks dontjee) - -## 103.4 - -* Version bump to fix assembly versioning - -## 103.3 - -* Added in the check for it being generic before calling GetGenericType Definition() (thanks nicwise) -* Add support for deserializing properties with an underscore prefix (thanks psampaio) -* BaseUrl is now virtual (thanks Haacked) -* Fixed List json deserialization, when T was a something like DateTime, Timespan, Guid, etc. (thanks PedroLamas) -* Improve support for parsing iso8601 dates (thanks friism) - -## 103.2 - -### New Features - -* Allow deserializing a single item into a List field, for JSON that only uses a list when there's more than one item for a given field (thanks petejohanson) -* Extended NtlmAuthenticator so that it can also impersonate a user (thanks kleinron) -* Added support for mapping JSON objects to Dictionary (thanks petejohanson) -* Added ability to set Host and Date when built for .NET 4.0 (thanks lukebakken) -* Allow deserializing lists with null in them. Should resolve pull request (thanks petejohanson) -* Add support for deserializing JSON to subclasses of List (thanks abaybuzskiy) - -### Bugs fixed -* Fixed invalid OAuth1 signature for GET request (thanks trilobyte) -* Added some missing OAuth files to the .NET4 and Silverlight projects (thanks PedroLamas) -* Removed unused NewtonsoftJsonMonoTouch.dll and Newtonsoft.Json.MonoDroid.dll binaries (thanks attilah) -* Fixed various issues with MonoTouch/Droid ports (thanks attilah) -* Add ability to set Host and Date when built for .NET 4.0 (thanks lukebakken) -* Fixed XmlDeserializer issue not handling lowercase + dash root elements in some cases -* Fixed an issue where RestResponse.Request was not populated (thanks mattleibow) -* Don't crash on captive networks that intercept SSL (thanks aroben) - -## 103.1 - -* #267 Added CLS Compliance -* #263 Fixed InvalidCastException -* #218 Handles connection failures better -* #231 OAuth now complies with rfc3986 url hex encoding - -## 103.0 - Remove dependency on Json.NET - -* Remove WP7.0 support (7.1 Mango remains). - -## 102.7 - -* Updating Json.NET to 4.0.8, misc fixes - -## 102.6 - -* Updating Json.NET reference to 4.0.5 \ No newline at end of file diff --git a/restsharp.png b/restsharp.png new file mode 100644 index 000000000..7203b38fd Binary files /dev/null and b/restsharp.png differ diff --git a/src/Directory.Build.props b/src/Directory.Build.props new file mode 100644 index 000000000..614781bf3 --- /dev/null +++ b/src/Directory.Build.props @@ -0,0 +1,41 @@ + + + + netstandard2.0;net471;net48;net8.0;net9.0;net10.0 + restsharp.png + Apache-2.0 + https://restsharp.dev + https://github.com/restsharp/RestSharp.git + git + Simple REST and HTTP API Client + .NET Foundation and Contributors + Copyright © $(Authors) $([System.DateTime]::Now.Year) + true + true + true + snupkg + true + $(NoWarn);1591 + README.md + + + + true + true + + + + + + + + + + + + + $(MinVerMajor).$(MinVerMinor).$(MinVerPatch) + $(MinVerMajor).$(MinVerMinor).$(MinVerPatch) + + + \ No newline at end of file diff --git a/src/RestSharp.Extensions.DependencyInjection/Constants.cs b/src/RestSharp.Extensions.DependencyInjection/Constants.cs new file mode 100644 index 000000000..058cad26e --- /dev/null +++ b/src/RestSharp.Extensions.DependencyInjection/Constants.cs @@ -0,0 +1,21 @@ +// 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. + +namespace RestSharp.Extensions.DependencyInjection; + +static class Constants { + public const string DefaultRestClient = "DefaultRestClient"; + + public static string GetConfigName(string name) => $"{name}$RestClient"; +} \ No newline at end of file diff --git a/src/RestSharp.Extensions.DependencyInjection/DefaultRestClientFactory.cs b/src/RestSharp.Extensions.DependencyInjection/DefaultRestClientFactory.cs new file mode 100644 index 000000000..124257f7e --- /dev/null +++ b/src/RestSharp.Extensions.DependencyInjection/DefaultRestClientFactory.cs @@ -0,0 +1,25 @@ +// 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. + +using Microsoft.Extensions.Options; + +namespace RestSharp.Extensions.DependencyInjection; + +class DefaultRestClientFactory(IHttpClientFactory httpClientFactory, IOptionsMonitor optionsMonitor) : IRestClientFactory { + public IRestClient CreateClient(string name) { + var options = optionsMonitor.Get(Constants.GetConfigName(name)); + var httpClient = httpClientFactory.CreateClient(name); + return new RestClient(httpClient, true, options.ConfigureRestClient, options.ConfigureSerialization); + } +} \ No newline at end of file diff --git a/src/RestSharp.Extensions.DependencyInjection/IRestClientFactory.cs b/src/RestSharp.Extensions.DependencyInjection/IRestClientFactory.cs new file mode 100644 index 000000000..2462e515a --- /dev/null +++ b/src/RestSharp.Extensions.DependencyInjection/IRestClientFactory.cs @@ -0,0 +1,19 @@ +// 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. + +namespace RestSharp.Extensions.DependencyInjection; + +public interface IRestClientFactory { + IRestClient CreateClient(string name); +} \ No newline at end of file diff --git a/src/RestSharp.Extensions.DependencyInjection/README.md b/src/RestSharp.Extensions.DependencyInjection/README.md new file mode 100644 index 000000000..498c5fac2 --- /dev/null +++ b/src/RestSharp.Extensions.DependencyInjection/README.md @@ -0,0 +1,45 @@ +# About + +The `RestSharp.Extensions.DependencyInjection` library provides integration with `Microsoft.Extensions.DependencyInjection` components as well as integrates with `IHttpClientFactory`. + +# How to use + +Use the extension method provided by the package to configure the client: + +```csharp +// Add a default client with no base URL, with default options +services.AddRestClient(); + +// Add a client with a base URL +services.AddRestClient(new Uri("https://example.com")); + +// Add a client with a base URL and custom options +services.AddRestClient(options => +{ + options.BaseUrl = new Uri("https://example.com"); + options.Timeout = TimeSpan.FromSeconds(30); +}); +``` + +When the above registrations are used, the `IRestClient` interface can be injected into any class. + +In addition, the package supports registering named clients: + +```csharp +services.AddRestClient("my-client", options => +{ + options.BaseUrl = new Uri("https://example.com"); + options.Timeout = TimeSpan.FromSeconds(30); +}); +``` + +When the above registrations are used, resolving the client instance should be done using the `IRestClientFactory`: + +```csharp +public class MyClass(IRestClientFactory restClientFactory) +{ + IRestClient client = restClientFactory.CreateClient("my-client"); + + // Use the client in your code +} +``` \ No newline at end of file diff --git a/src/RestSharp.Extensions.DependencyInjection/RestClientConfigOptions.cs b/src/RestSharp.Extensions.DependencyInjection/RestClientConfigOptions.cs new file mode 100644 index 000000000..6ba35f6ce --- /dev/null +++ b/src/RestSharp.Extensions.DependencyInjection/RestClientConfigOptions.cs @@ -0,0 +1,20 @@ +// 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. + +namespace RestSharp.Extensions.DependencyInjection; + +class RestClientConfigOptions { + public ConfigureRestClient? ConfigureRestClient { get; set; } + public ConfigureSerialization? ConfigureSerialization { get; set; } +} \ No newline at end of file diff --git a/src/RestSharp.Extensions.DependencyInjection/RestSharp.Extensions.DependencyInjection.csproj b/src/RestSharp.Extensions.DependencyInjection/RestSharp.Extensions.DependencyInjection.csproj new file mode 100644 index 000000000..32210a920 --- /dev/null +++ b/src/RestSharp.Extensions.DependencyInjection/RestSharp.Extensions.DependencyInjection.csproj @@ -0,0 +1,15 @@ + + + + + + + + + + + true + / + + + diff --git a/src/RestSharp.Extensions.DependencyInjection/ServiceCollectionExtensions.cs b/src/RestSharp.Extensions.DependencyInjection/ServiceCollectionExtensions.cs new file mode 100644 index 000000000..d1f13c36d --- /dev/null +++ b/src/RestSharp.Extensions.DependencyInjection/ServiceCollectionExtensions.cs @@ -0,0 +1,107 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace RestSharp.Extensions.DependencyInjection; + +public static class ServiceCollectionExtensions { + extension(IServiceCollection services) { + /// + /// Adds a named RestClient to the service collection. + /// + /// Client name + /// Optional: function to configure the client options. + /// Optional: function to configure serializers. + [PublicAPI] + public void AddRestClient( + string name, + ConfigureRestClient? configureRestClient = null, + ConfigureSerialization? configureSerialization = null + ) { + Ensure.NotEmptyString(name, nameof(name)); + Ensure.NotNull(services, nameof(services)); + + var options = new RestClientOptions(); + var configure = configureRestClient ?? (_ => { }); + configure(options); + + services + .AddHttpClient(name) + .ConfigureHttpClient(client => RestClient.ConfigureHttpClient(client, options)) + .ConfigurePrimaryHttpMessageHandler(() => { + var handler = new HttpClientHandler(); + RestClient.ConfigureHttpMessageHandler(handler, options); + return handler; + } + ); + + services.TryAddSingleton(); + + if (name == Constants.DefaultRestClient) { + services.AddTransient(sp => { + var client = sp.GetRequiredService().CreateClient(name); + return new RestClient(client, options); + } + ); + } + else { + services.Configure( + Constants.GetConfigName(name), + o => { + o.ConfigureRestClient = configureRestClient; + o.ConfigureSerialization = configureSerialization; + } + ); + } + } + + /// + /// Adds a RestClient to the service collection with default options. + /// + [PublicAPI] + public void AddRestClient() => services.AddRestClient(Constants.DefaultRestClient); + + /// + /// Adds a RestClient to the service collection with a base URL. + /// + /// The base URL for the RestClient. + [PublicAPI] + public void AddRestClient(Uri baseUrl) => services.AddRestClient(Constants.DefaultRestClient, o => o.BaseUrl = baseUrl); + + /// + /// Adds a RestClient to the service collection with custom options. + /// + /// Custom options for the RestClient. + [PublicAPI] + public void AddRestClient(RestClientOptions options) { + Ensure.NotNull(options, nameof(options)); + services.AddRestClient(Constants.DefaultRestClient, o => o.CopyFrom(options)); + } + + /// + /// Adds a RestClient to the service collection with custom options. + /// + /// Function to configure the RestClient options. + [PublicAPI] + public void AddRestClient(ConfigureRestClient configureRestClient) { + Ensure.NotNull(configureRestClient, nameof(configureRestClient)); + services.AddRestClient(Constants.DefaultRestClient, configureRestClient); + } + + /// + /// Adds a named RestClient to the service collection with base URL. + /// + /// Client name. + /// The base URL for the RestClient. + public void AddRestClient(string name, Uri baseUrl) => services.AddRestClient(name, o => o.BaseUrl = baseUrl); + + /// + /// Adds a named RestClient to the service collection with custom options. + /// + /// Client name. + /// Custom options for the RestClient. + public void AddRestClient(string name, RestClientOptions options) { + Ensure.NotNull(options, nameof(options)); + services.AddRestClient(name, o => o.CopyFrom(options)); + } + } +} \ No newline at end of file diff --git a/src/RestSharp.Serializers.CsvHelper/CsvHelperSerializer.cs b/src/RestSharp.Serializers.CsvHelper/CsvHelperSerializer.cs new file mode 100644 index 000000000..54e82ab2a --- /dev/null +++ b/src/RestSharp.Serializers.CsvHelper/CsvHelperSerializer.cs @@ -0,0 +1,89 @@ +using CsvHelper; +using CsvHelper.Configuration; +using System.Collections; +using System.Globalization; + +namespace RestSharp.Serializers.CsvHelper; + +public class CsvHelperSerializer(CsvConfiguration configuration) : IDeserializer, IRestSerializer, ISerializer { + public ISerializer Serializer => this; + + public IDeserializer Deserializer => this; + + public string[] AcceptedContentTypes => [ContentType.Csv, "application/x-download"]; + + public SupportsContentType SupportsContentType => x => Array.IndexOf(AcceptedContentTypes, x) != -1 || x.Value.Contains("csv"); + + public DataFormat DataFormat => DataFormat.None; + + public ContentType ContentType { get; set; } = ContentType.Csv; + + public CsvHelperSerializer() : this(new(CultureInfo.InvariantCulture)) { } + + public T? Deserialize(RestResponse response) { + try { + if (response.Content == null) + throw new InvalidOperationException(message: "Response content is null"); + + using var stringReader = new StringReader(response.Content); + using var csvReader = new CsvReader(stringReader, configuration); + + var @interface = typeof(T).GetInterface("IEnumerable`1"); + + if (@interface == null) { + csvReader.Read(); + return csvReader.GetRecord(); + } + + var itemType = @interface.GenericTypeArguments[0]; + T result; + + try { + result = Activator.CreateInstance(); + } + catch (MissingMethodException) { + throw new InvalidOperationException(message: "The type must contain a public, parameterless constructor."); + } + + var method = typeof(T).GetMethod(name: "Add"); + + if (method == null) { + throw new InvalidOperationException( + message: "If the type implements IEnumerable, then it must contain a public \"Add(T)\" method." + ); + } + + foreach (var record in csvReader.GetRecords(itemType)) { + method.Invoke(result, [record]); + } + + return result; + } + catch (Exception exception) { + throw new DeserializationException(response, exception); + } + } + + public string? Serialize(Parameter parameter) => Serialize(parameter.Value); + + public string? Serialize(object? obj) { + if (obj == null) { + return null; + } + + using var stringWriter = new StringWriter(); + using var csvWriter = new CsvWriter(stringWriter, configuration); + + if (obj is IEnumerable records) { + csvWriter.WriteRecords(records); + } + else { + csvWriter.WriteHeader(obj.GetType()); + csvWriter.NextRecord(); + csvWriter.WriteRecord(obj); + csvWriter.NextRecord(); + } + + return stringWriter.ToString(); + } +} \ No newline at end of file diff --git a/src/RestSharp.Serializers.CsvHelper/README.md b/src/RestSharp.Serializers.CsvHelper/README.md new file mode 100644 index 000000000..3c7d3815c --- /dev/null +++ b/src/RestSharp.Serializers.CsvHelper/README.md @@ -0,0 +1,24 @@ +# About + +The `RestSharp.Serializers.CsvHelper` library provides a CSV serializer for RestSharp. It is based on the +`CsvHelper` library. + +# How to use + +Use the extension method provided by the package to configure the client: + +```csharp +var client = new RestClient( + options, + configureSerialization: s => s.UseCsvHelper() +); +``` + +You can also supply your instance of `CsvConfiguration` as a parameter for the extension method. + +```csharp +var client = new RestClient( + options, + configureSerialization: s => s.UseCsvHelper(new CsvConfiguration(CultureInfo.InvariantCulture) {...}) +); +``` diff --git a/src/RestSharp.Serializers.CsvHelper/RestClientExtensions.cs b/src/RestSharp.Serializers.CsvHelper/RestClientExtensions.cs new file mode 100644 index 000000000..4d6456b76 --- /dev/null +++ b/src/RestSharp.Serializers.CsvHelper/RestClientExtensions.cs @@ -0,0 +1,13 @@ +using CsvHelper.Configuration; + +namespace RestSharp.Serializers.CsvHelper; + +[PublicAPI] +public static class RestClientExtensions { + extension(SerializerConfig config) { + public SerializerConfig UseCsvHelper() => config.UseSerializer(); + + public SerializerConfig UseCsvHelper(CsvConfiguration configuration) + => config.UseSerializer(() => new CsvHelperSerializer(configuration)); + } +} diff --git a/src/RestSharp.Serializers.CsvHelper/RestSharp.Serializers.CsvHelper.csproj b/src/RestSharp.Serializers.CsvHelper/RestSharp.Serializers.CsvHelper.csproj new file mode 100644 index 000000000..9c503dd50 --- /dev/null +++ b/src/RestSharp.Serializers.CsvHelper/RestSharp.Serializers.CsvHelper.csproj @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/RestSharp.Serializers.NewtonsoftJson/JsonNetSerializer.cs b/src/RestSharp.Serializers.NewtonsoftJson/JsonNetSerializer.cs new file mode 100644 index 000000000..d5ee33046 --- /dev/null +++ b/src/RestSharp.Serializers.NewtonsoftJson/JsonNetSerializer.cs @@ -0,0 +1,83 @@ +// 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. + +using Newtonsoft.Json.Serialization; + +namespace RestSharp.Serializers.NewtonsoftJson; + +public class JsonNetSerializer : IRestSerializer, ISerializer, IDeserializer { + /// + /// Default serialization settings: + /// - Camel-case contract resolver + /// - Type name handling set to none + /// - Null values ignored + /// - Non-indented formatting + /// - Allow using non-public constructors + /// + public static readonly JsonSerializerSettings DefaultSettings = new() { + ContractResolver = new CamelCasePropertyNamesContractResolver(), + DefaultValueHandling = DefaultValueHandling.Include, + TypeNameHandling = TypeNameHandling.None, + NullValueHandling = NullValueHandling.Ignore, + Formatting = Formatting.None, + ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor + }; + + [ThreadStatic] static WriterBuffer? _writerBuffer; + + readonly JsonSerializer _serializer; + + /// + /// Create the new serializer that uses Json.Net with default settings + /// + public JsonNetSerializer() => _serializer = JsonSerializer.Create(DefaultSettings); + + /// + /// Create the new serializer that uses Json.Net with custom settings + /// + /// Json.Net serializer settings + public JsonNetSerializer(JsonSerializerSettings settings) => _serializer = JsonSerializer.Create(settings); + + public string? Serialize(object? obj) { + if (obj == null) return null; + + using var buffer = _writerBuffer ??= new(_serializer); + + _serializer.Serialize(buffer.GetJsonTextWriter(), obj, obj.GetType()); + + return buffer.GetStringWriter().ToString(); + } + + public string? Serialize(Parameter bodyParameter) => Serialize(bodyParameter.Value); + + public T? Deserialize(RestResponse response) { + if (response.Content == null) + throw new DeserializationException(response, new InvalidOperationException("Response content is null")); + + using var reader = new JsonTextReader(new StringReader(response.Content)) { CloseInput = true }; + + return _serializer.Deserialize(reader); + } + + public ISerializer Serializer => this; + public IDeserializer Deserializer => this; + + public string[] AcceptedContentTypes => ContentType.JsonAccept; + + public ContentType ContentType { get; set; } = ContentType.Json; + + public SupportsContentType SupportsContentType => contentType => contentType.Value.Contains("json"); + + public DataFormat DataFormat => DataFormat.Json; +} \ No newline at end of file diff --git a/src/RestSharp.Serializers.NewtonsoftJson/README.md b/src/RestSharp.Serializers.NewtonsoftJson/README.md new file mode 100644 index 000000000..5cc11a71b --- /dev/null +++ b/src/RestSharp.Serializers.NewtonsoftJson/README.md @@ -0,0 +1,19 @@ +# About + +This library allows using Newtonsoft.Json as a serializer for RestSharp instead of the default JSON serializer based +on `System.Text.Json`. + +# How to use + +The default JSON serializer uses `System.Text.Json`, which is a part of .NET since .NET 6. + +If you want to use Newtonsoft.Json, you can install the `RestSharp.Serializers.NewtonsoftJson` package and configure +the +client to use it: + +```csharp +var client = new RestClient( + options, + configureSerialization: s => s.UseNewtonsoftJson() +); +``` \ No newline at end of file diff --git a/src/RestSharp.Serializers.NewtonsoftJson/RestClientExtensions.cs b/src/RestSharp.Serializers.NewtonsoftJson/RestClientExtensions.cs new file mode 100644 index 000000000..7f6e6ffec --- /dev/null +++ b/src/RestSharp.Serializers.NewtonsoftJson/RestClientExtensions.cs @@ -0,0 +1,35 @@ +// 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. + +namespace RestSharp.Serializers.NewtonsoftJson; + +[PublicAPI] +public static class RestClientExtensions { + /// + extension(SerializerConfig config) { + /// + /// Use Newtonsoft.Json serializer with default settings + /// + /// + public SerializerConfig UseNewtonsoftJson() => config.UseSerializer(() => new JsonNetSerializer()); + + /// + /// Use Newtonsoft.Json serializer with custom settings + /// + /// Newtonsoft.Json serializer settings + /// + public SerializerConfig UseNewtonsoftJson(JsonSerializerSettings settings) + => config.UseSerializer(() => new JsonNetSerializer(settings)); + } +} \ No newline at end of file diff --git a/src/RestSharp.Serializers.NewtonsoftJson/RestSharp.Serializers.NewtonsoftJson.csproj b/src/RestSharp.Serializers.NewtonsoftJson/RestSharp.Serializers.NewtonsoftJson.csproj new file mode 100644 index 000000000..45bcf8ca9 --- /dev/null +++ b/src/RestSharp.Serializers.NewtonsoftJson/RestSharp.Serializers.NewtonsoftJson.csproj @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/RestSharp.Serializers.NewtonsoftJson/WriterBuffer.cs b/src/RestSharp.Serializers.NewtonsoftJson/WriterBuffer.cs new file mode 100644 index 000000000..a1505a869 --- /dev/null +++ b/src/RestSharp.Serializers.NewtonsoftJson/WriterBuffer.cs @@ -0,0 +1,36 @@ +// 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. + +using System.Globalization; + +namespace RestSharp.Serializers.NewtonsoftJson; + +sealed class WriterBuffer : IDisposable { + readonly StringWriter _stringWriter; + readonly JsonTextWriter _jsonTextWriter; + + public WriterBuffer(JsonSerializer jsonSerializer) { + _stringWriter = new(new(256), CultureInfo.InvariantCulture); + + _jsonTextWriter = new(_stringWriter) { + Formatting = jsonSerializer.Formatting, CloseOutput = false + }; + } + + public JsonTextWriter GetJsonTextWriter() => _jsonTextWriter; + + public StringWriter GetStringWriter() => _stringWriter; + + public void Dispose() => _stringWriter.GetStringBuilder().Clear(); +} \ No newline at end of file diff --git a/src/RestSharp.Serializers.Xml/DeserializeAsAttribute.cs b/src/RestSharp.Serializers.Xml/DeserializeAsAttribute.cs new file mode 100644 index 000000000..ecb235e90 --- /dev/null +++ b/src/RestSharp.Serializers.Xml/DeserializeAsAttribute.cs @@ -0,0 +1,37 @@ +// 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. + +// ReSharper disable once CheckNamespace +namespace RestSharp.Serializers; + +/// +/// Allows control how class and property names and values are deserialized by XmlAttributeDeserializer +/// +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Class, Inherited = false)] +public sealed class DeserializeAsAttribute : Attribute { + /// + /// The name to use for the serialized element + /// + public string? Name { get; set; } + + /// + /// Sets if the property to Deserialize is an Attribute or Element (Default: false) + /// + public bool Attribute { get; set; } + + /// + /// Sets if the property to Deserialize is a content of current Element (Default: false) + /// + public bool Content { get; set; } +} \ No newline at end of file diff --git a/src/RestSharp.Serializers.Xml/README.md b/src/RestSharp.Serializers.Xml/README.md new file mode 100644 index 000000000..63a7f53cc --- /dev/null +++ b/src/RestSharp.Serializers.Xml/README.md @@ -0,0 +1,22 @@ +# About + +This package is a custom XML serializer for RestSharp. It is based on the original XML serializer that was part of RestSharp but was removed in version 107.0.0. + +# How to use + +The default XML serializer in RestSharp is `DotNetXmlSerializer`, which uses `System.Xml.Serialization` library from . +NET. + +In previous versions of RestSharp, the default XML serializer was a custom RestSharp XML serializer. To make the +code library size smaller, the custom serializer was removed from RestSharp. + +You can add it back if necessary by installing the `RestSharp.Serializers.Xml` package and adding it to the client: + +```csharp +var client = new RestClient( + options, + configureSerialization: s => s.UseXmlSerializer() +); +``` + +As before, you can supply three optional arguments for a custom namespace, custom root element, and if you want to use `SerializeAs` and `DeserializeAs` attributed. diff --git a/src/RestSharp.Serializers.Xml/RestSharp.Serializers.Xml.csproj b/src/RestSharp.Serializers.Xml/RestSharp.Serializers.Xml.csproj new file mode 100644 index 000000000..42ee74306 --- /dev/null +++ b/src/RestSharp.Serializers.Xml/RestSharp.Serializers.Xml.csproj @@ -0,0 +1,14 @@ + + + RestSharp.Serializers.Xml + + + + + + + + + + + diff --git a/src/RestSharp.Serializers.Xml/SerializeAsAttribute.cs b/src/RestSharp.Serializers.Xml/SerializeAsAttribute.cs new file mode 100644 index 000000000..598f446b5 --- /dev/null +++ b/src/RestSharp.Serializers.Xml/SerializeAsAttribute.cs @@ -0,0 +1,80 @@ +// 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. + +using System.Globalization; +using RestSharp.Extensions; +// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global +// ReSharper disable MemberCanBePrivate.Global + +// ReSharper disable once CheckNamespace +namespace RestSharp.Serializers; + +/// +/// Allows control how class and property names and values are serialized by XmlSerializer +/// Currently not supported with the JsonSerializer +/// When specified at the property level the class-level specification is overridden +/// +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Class, Inherited = false)] +public sealed class SerializeAsAttribute : Attribute { + /// + /// The name to use for the serialized element + /// + public string? Name { get; set; } + + /// + /// Sets the value to be serialized as an Attribute instead of an Element + /// + public bool Attribute { get; set; } + + /// + /// Sets the value to be serialized as text content of current Element instead of an new Element + /// + public bool Content { get; set; } + + /// + /// The culture to use when serializing + /// + public CultureInfo Culture { get; set; } = CultureInfo.InvariantCulture; + + /// + /// Transforms the casing of the name based on the selected value. + /// + public NameStyle NameStyle { get; set; } = NameStyle.AsIs; + + /// + /// The order to serialize the element. Default is int.MaxValue. + /// + public int Index { get; set; } = int.MaxValue; + + /// + /// Called by the attribute when NameStyle is speficied + /// + /// The string to transform + /// String + public string TransformName(string input) { + var name = Name ?? input; + + return NameStyle switch { + NameStyle.CamelCase => name.ToCamelCase(Culture), + NameStyle.PascalCase => name.ToPascalCase(Culture), + NameStyle.LowerCase => name.ToLower(Culture), + _ => input + }; + } +} + +/// +/// Options for transforming casing of element names +/// +public enum NameStyle { AsIs, CamelCase, LowerCase, PascalCase } \ No newline at end of file diff --git a/src/RestSharp.Serializers.Xml/XmlAttributeDeserializer.cs b/src/RestSharp.Serializers.Xml/XmlAttributeDeserializer.cs new file mode 100644 index 000000000..b2677f014 --- /dev/null +++ b/src/RestSharp.Serializers.Xml/XmlAttributeDeserializer.cs @@ -0,0 +1,39 @@ +// 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. + +using System.Reflection; +using System.Xml.Linq; +using RestSharp.Extensions; + +namespace RestSharp.Serializers.Xml; + +public class XmlAttributeDeserializer : XmlDeserializer { + protected override object? GetValueFromXml(XElement? root, XName? name, PropertyInfo prop, bool useExactName) { + var isAttribute = false; + + //Check for the DeserializeAs attribute on the property + var options = prop.GetAttribute(); + + if (options != null) { + name = options.Name ?? name; + isAttribute = options.Attribute; + } + + if (!isAttribute) return base.GetValueFromXml(root, name, prop, useExactName); + + var attributeVal = GetAttributeByName(root!, name!, useExactName); + + return attributeVal?.Value ?? base.GetValueFromXml(root, name, prop, useExactName); + } +} \ No newline at end of file diff --git a/src/RestSharp.Serializers.Xml/XmlDeserializer.cs b/src/RestSharp.Serializers.Xml/XmlDeserializer.cs new file mode 100644 index 000000000..4b5a43d35 --- /dev/null +++ b/src/RestSharp.Serializers.Xml/XmlDeserializer.cs @@ -0,0 +1,531 @@ +// 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. + +using System.Collections; +using System.ComponentModel; +using System.Globalization; +using System.Reflection; +using System.Xml; +using System.Xml.Linq; +using RestSharp.Extensions; +// ReSharper disable VirtualMemberNeverOverridden.Global + +namespace RestSharp.Serializers.Xml; + +public class XmlDeserializer : IXmlDeserializer, IWithRootElement, IWithDateFormat { + public CultureInfo Culture { get; set; } = CultureInfo.InvariantCulture; + + public string? RootElement { get; set; } + + public string? Namespace { get; set; } + + public string? DateFormat { get; set; } + + public virtual T? Deserialize(RestResponse response) { + if (string.IsNullOrEmpty(response.Content)) + return default; + + var doc = XDocument.Parse(response.Content!); + var root = doc.Root; + var rootElement = response.RootElement ?? RootElement; + + if (rootElement != null && doc.Root != null) { + var namespacedRoot = rootElement.AsNamespaced(Namespace); + // Prefer the shallowest match to avoid nested elements with the same name + root = namespacedRoot != null + ? doc.Root.Element(namespacedRoot) + ?? doc.Root.DescendantsAndSelf(namespacedRoot) + .OrderBy(e => e.Ancestors().Count()) + .FirstOrDefault() + : null; + } + + // autodetect xml namespace + if (Namespace.IsEmpty()) + RemoveNamespace(doc); + + var x = Activator.CreateInstance(); + var objType = x!.GetType(); + + if (objType.IsSubclassOfRawGeneric(typeof(List<>))) + x = (T)HandleListDerivative(root!, objType.Name, objType); + else + x = (T)Map(x, root!); + + return x; + } + + static void RemoveNamespace(XDocument xdoc) { + if (xdoc.Root == null) return; + + foreach (var e in xdoc.Root.DescendantsAndSelf()) { + if (e.Name.Namespace != XNamespace.None) + e.Name = XNamespace.None.GetName(e.Name.LocalName); + + if (!e.Attributes().Any(a => a.IsNamespaceDeclaration || a.Name.Namespace != XNamespace.None)) continue; + + e.ReplaceAttributes( + e.Attributes() + .Select( + a => a.IsNamespaceDeclaration + ? null + : a.Name.Namespace != XNamespace.None + ? new(XNamespace.None.GetName(a.Name.LocalName), a.Value) + : a + ) + .Where(a => a != null) + ); + } + } + + static bool IsValidXmlElementName(string name) => + // Generic type names contain backtick (e.g., "List`1") which is invalid in XML element names + !name.Contains('`'); + + protected virtual object Map(object x, XElement? root) { + var objType = x.GetType(); + var props = objType.GetProperties(); + + var deserializeFromContentAttributeAlreadyUsed = false; + + foreach (var prop in props) { + var type = prop.PropertyType.GetTypeInfo(); + var typeIsPublic = type.IsPublic || type.IsNestedPublic; + + if (!typeIsPublic || !prop.CanWrite) + continue; + + var deserializeFromContent = false; + var isNameDefinedInAttribute = false; + XName? name = null; + var attributes = prop.GetCustomAttributes(typeof(DeserializeAsAttribute), false); + + if (attributes.Any()) { + var attribute = (DeserializeAsAttribute)attributes.First(); + + name = attribute.Name.AsNamespaced(Namespace); + isNameDefinedInAttribute = !string.IsNullOrEmpty(name?.LocalName); + + deserializeFromContent = attribute.Content; + + if (deserializeFromContentAttributeAlreadyUsed && deserializeFromContent) + throw new ArgumentException("Class cannot have two properties marked with SerializeAs(Content = true) attribute."); + + deserializeFromContentAttributeAlreadyUsed |= deserializeFromContent; + } + + if (name == null) name = prop.Name.AsNamespaced(Namespace); + + var value = GetValueFromXml(root, name, prop, isNameDefinedInAttribute); + + if (value == null) { + // special case for text content node + if (deserializeFromContent) { + var textNode = root!.Nodes().FirstOrDefault(n => n is XText); + + if (textNode != null) { + value = ((XText)textNode).Value; + prop.SetValue(x, value, null); + } + + continue; + } + + // special case for inline list items + if (type.IsGenericType) { + var genericType = type.GetGenericArguments()[0]; + var first = GetElementByName(root, genericType.Name); + var list = (IList)Activator.CreateInstance(type.AsType())!; + + if (first != null && root != null) { + var elements = root.Elements(first.Name); + + PopulateListFromElements(genericType, elements, list); + } + + prop.SetValue(x, list, null); + } + + continue; + } + + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) { + // if the value is empty, set the property to null... + if (string.IsNullOrEmpty(value.ToString())) { + prop.SetValue(x, null, null); + continue; + } + + type = type.GetGenericArguments()[0].GetTypeInfo(); + } + + var asType = type.AsType(); + + if (asType == typeof(bool)) { + var toConvert = value.ToString()!.ToLower(Culture); + + prop.SetValue(x, XmlConvert.ToBoolean(toConvert), null); + } + else if (type.IsPrimitive) { + try { + prop.SetValue(x, value.ChangeType(asType), null); + } + catch (FormatException ex) { + throw new FormatException( + $"Couldn't parse the value of '{value}' into the '{prop.Name}'" + + $" property, because it isn't a type of '{prop.PropertyType}'.", + ex.InnerException + ); + } + } + else if (type.IsEnum) { + var converted = type.AsType().FindEnumValue(value.ToString(), Culture); + + prop.SetValue(x, converted, null); + } + else if (asType == typeof(Uri)) { + var uri = new Uri(value.ToString(), UriKind.RelativeOrAbsolute); + + prop.SetValue(x, uri, null); + } + else if (asType == typeof(string)) { + prop.SetValue(x, value, null); + } + else if (asType == typeof(DateTime)) { + value = DateFormat.IsNotEmpty() + ? DateTime.ParseExact(value.ToString(), DateFormat!, Culture) + : DateTime.Parse(value.ToString(), Culture); + + prop.SetValue(x, value, null); + } + else if (asType == typeof(DateTimeOffset)) { + var toConvert = value.ToString(); + + if (string.IsNullOrEmpty(toConvert)) continue; + + DateTimeOffset deserialisedValue; + + try { + deserialisedValue = XmlConvert.ToDateTimeOffset(toConvert); + prop.SetValue(x, deserialisedValue, null); + } + catch (Exception) { + if (TryGetFromString(toConvert, out var result, asType)) { + prop.SetValue(x, result, null); + } + else { + //fallback to parse + deserialisedValue = DateTimeOffset.Parse(toConvert); + prop.SetValue(x, deserialisedValue, null); + } + } + } + else if (asType == typeof(decimal)) { + value = decimal.Parse(value.ToString(), Culture); + prop.SetValue(x, value, null); + } + else if (asType == typeof(Guid)) { + var raw = value.ToString(); + + value = string.IsNullOrEmpty(raw) ? Guid.Empty : new(value.ToString()); + + prop.SetValue(x, value, null); + } + else if (asType == typeof(TimeSpan)) { + var timeSpan = XmlConvert.ToTimeSpan(value.ToString()); + + prop.SetValue(x, timeSpan, null); + } + else if (type.IsGenericType) { + var list = (IList)Activator.CreateInstance(asType)!; + XElement? container = null; + + // First check if root itself is the container (matches property name) + if (root != null && name?.LocalName != null) { + var rootName = root.Name.LocalName; + if (rootName.Equals(name.LocalName, StringComparison.OrdinalIgnoreCase)) { + container = root; + } + } + + // If root is not the container, try to find it as a child element + container ??= GetElementByName(root, name); + + if (container?.HasElements == true) { + var first = container.Elements().FirstOrDefault(); + + if (first != null) { + var t = type.GetGenericArguments()[0]; + var elements = container.Elements(first.Name); + + PopulateListFromElements(t, elements, list); + } + } + + prop.SetValue(x, list, null); + } + else if (asType.IsSubclassOfRawGeneric(typeof(List<>))) { + // handles classes that derive from List + // e.g. a collection that also has attributes + var list = HandleListDerivative(root!, prop.Name, asType); + + prop.SetValue(x, list, null); + } + else { + //fallback to type converters if possible + + if (TryGetFromString(value.ToString(), out var result, asType)) { + prop.SetValue(x, result, null); + } + else { + // nested property classes + if (root == null) continue; + + var element = GetElementByName(root, name); + + if (element == null) continue; + + var item = CreateAndMap(asType, element); + + prop.SetValue(x, item, null); + } + } + } + + return x; + } + + static bool TryGetFromString(string inputString, out object? result, Type type) { + var converter = TypeDescriptor.GetConverter(type); + + if (converter.CanConvertFrom(typeof(string))) { + result = converter.ConvertFromInvariantString(inputString); + return true; + } + + result = null; + return false; + } + + void PopulateListFromElements(Type t, IEnumerable elements, IList list) { + foreach (var item in elements.Select(element => CreateAndMap(t, element))) + list.Add(item); + } + + object HandleListDerivative(XElement root, string propName, Type type) { + var t = type.IsGenericType + ? type.GetGenericArguments()[0] + : type.BaseType!.GetGenericArguments()[0]; + + var list = (IList)Activator.CreateInstance(type)!; + + var name = t.Name; + var attribute = t.GetAttribute(); + + if (attribute != null) + name = attribute.Name; + + // Try to find a container element first using the property name + XElement? container = null; + + // Try the property name first (skip if it contains invalid XML name characters like ` in generic types) + if (IsValidXmlElementName(propName)) { + container = GetElementByName(root, propName.AsNamespaced(Namespace)); + } + + // Check if root itself matches the container naming + if (container == null && IsValidXmlElementName(propName)) { + var rootName = root.Name.LocalName; + + if (rootName.Equals(propName, StringComparison.OrdinalIgnoreCase)) { + container = root; + } + } + + IList elements; + + // If we found a specific container, use Elements() to get only direct children + // This prevents nested lists from being incorrectly flattened + if (container is { HasElements: true }) { + elements = TryFindElementsByNameVariations(container, t.Name, name, useDirectChildrenOnly: true); + } + else { + // No container found - use Descendants() for backward compatibility + // This handles cases where items are nested at varying depths without a clear container + elements = TryFindElementsByNameVariations(root, t.Name, name, useDirectChildrenOnly: false); + } + + PopulateListFromElements(t, elements, list); + + // get properties too, not just list items + // only if this isn't a generic type + if (!type.IsGenericType) + Map(list, root.Element(propName.AsNamespaced(Namespace)!) ?? root); + + return list; + } + + IList TryFindElementsByNameVariations(XElement source, string typeName, string? itemName, bool useDirectChildrenOnly) { + IList elements; + + if (useDirectChildrenOnly) { + // Use Elements() for direct children only + elements = source.Elements(typeName.AsNamespaced(Namespace)).ToList(); + + if (!elements.Any()) { + var lowerName = itemName?.ToLower(Culture).AsNamespaced(Namespace); + elements = source.Elements(lowerName).ToList(); + } + + if (!elements.Any()) { + var camelName = itemName?.ToCamelCase(Culture).AsNamespaced(Namespace); + elements = source.Elements(camelName).ToList(); + } + + if (!elements.Any()) { + elements = source.Elements() + .Where(e => e.Name.LocalName.RemoveUnderscoresAndDashes() == itemName) + .ToList(); + } + + if (!elements.Any()) { + var lowerName = itemName?.ToLower(Culture); + elements = source.Elements() + .Where(e => e.Name.LocalName.RemoveUnderscoresAndDashes() == lowerName) + .ToList(); + } + } + else { + // Use Descendants() for backward compatibility when no container is found + elements = source.Descendants(typeName.AsNamespaced(Namespace)).ToList(); + + if (!elements.Any()) { + var lowerName = itemName?.ToLower(Culture).AsNamespaced(Namespace); + elements = source.Descendants(lowerName).ToList(); + } + + if (!elements.Any()) { + var camelName = itemName?.ToCamelCase(Culture).AsNamespaced(Namespace); + elements = source.Descendants(camelName).ToList(); + } + + if (!elements.Any()) { + elements = source.Descendants() + .Where(e => e.Name.LocalName.RemoveUnderscoresAndDashes() == itemName) + .ToList(); + } + + if (!elements.Any()) { + var lowerName = itemName?.ToLower(Culture); + elements = source.Descendants() + .Where(e => e.Name.LocalName.RemoveUnderscoresAndDashes() == lowerName) + .ToList(); + } + } + + return elements; + } + + protected virtual object? CreateAndMap(Type t, XElement element) { + object? item; + + if (t == typeof(string)) { + item = element.Value; + } + else if (t.GetTypeInfo().IsPrimitive) { + item = element.Value.ChangeType(t); + } + else { + item = Activator.CreateInstance(t)!; + Map(item, element); + } + + return item; + } + + protected virtual object? GetValueFromXml(XElement? root, XName? name, PropertyInfo prop, bool useExactName) { + object? val = null; + if (root == null) return val; + + var element = GetElementByName(root, name); + + if (element == null) { + var attribute = GetAttributeByName(root, name!, useExactName); + + if (attribute != null) + val = attribute.Value; + } + else { + if (!element.IsEmpty || element.HasElements || element.HasAttributes) + val = element.Value; + } + + return val; + } + + protected virtual XElement? GetElementByName(XElement? root, XName? name) { + var lowerName = name?.LocalName.ToLower(Culture).AsNamespaced(name.NamespaceName); + var camelName = name?.LocalName.ToCamelCase(Culture).AsNamespaced(name.NamespaceName); + + if (root?.Element(name!) != null) + return root.Element(name!); + + if (root?.Element(lowerName!) != null) + return root.Element(lowerName!); + + if (root?.Element(camelName!) != null) + return root.Element(camelName!); + + // try looking for element that matches sanitized property name (Order by depth) + var orderedDescendants = root!.Descendants() + .OrderBy(d => d.Ancestors().Count()) + .ToList(); + + var element = orderedDescendants + .FirstOrDefault(d => d.Name.LocalName.RemoveUnderscoresAndDashes() == name?.LocalName) ?? + orderedDescendants + .FirstOrDefault( + d => string.Equals( + d.Name.LocalName.RemoveUnderscoresAndDashes(), + name?.LocalName, + StringComparison.OrdinalIgnoreCase + ) + ); + + return element == null && + name == "Value".AsNamespaced(name?.NamespaceName) && + (!root.HasAttributes || root.Attributes().All(x => x.Name != name)) + ? root + : element; + } + + protected virtual XAttribute? GetAttributeByName(XElement root, XName name, bool useExactName) { + var names = useExactName + ? null + : new List { + name.LocalName, + name.LocalName.ToLower(Culture).AsNamespaced(name.NamespaceName)!, + name.LocalName.ToCamelCase(Culture).AsNamespaced(name.NamespaceName)! + }; + + return root.DescendantsAndSelf() + .OrderBy(d => d.Ancestors().Count()) + .Attributes() + .FirstOrDefault( + d => useExactName + ? d.Name == name + : names?.Contains(d.Name.LocalName.RemoveUnderscoresAndDashes()) == true + ); + } +} \ No newline at end of file diff --git a/src/RestSharp.Serializers.Xml/XmlExtensions.cs b/src/RestSharp.Serializers.Xml/XmlExtensions.cs new file mode 100644 index 000000000..b988f7abb --- /dev/null +++ b/src/RestSharp.Serializers.Xml/XmlExtensions.cs @@ -0,0 +1,37 @@ +// 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. + +using System.Xml.Linq; +using RestSharp.Extensions; + +namespace RestSharp.Serializers.Xml; + +/// +/// XML Extension Methods +/// +public static class XmlExtensions { + /// + /// Returns the name of an element with the namespace if specified + /// + /// Element name + /// XML Namespace + /// + public static XName? AsNamespaced(this string? name, string? @namespace) { + XName? xName = name; + + if (name != null && @namespace.IsNotEmpty()) xName = XName.Get(name, @namespace); + + return xName; + } +} \ No newline at end of file diff --git a/src/RestSharp.Serializers.Xml/XmlSerializer.cs b/src/RestSharp.Serializers.Xml/XmlSerializer.cs new file mode 100644 index 000000000..170954c28 --- /dev/null +++ b/src/RestSharp.Serializers.Xml/XmlSerializer.cs @@ -0,0 +1,245 @@ +// 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. + +using System.Collections; +using System.Globalization; +using System.Reflection; +using System.Xml.Linq; +using RestSharp.Extensions; + +namespace RestSharp.Serializers.Xml; + +/// +/// Default XML Serializer +/// +public class XmlSerializer : IXmlSerializer, IWithRootElement, IWithDateFormat { + /// + /// Default constructor, does not specify namespace + /// + public XmlSerializer() { } + + /// + /// Specify the namespaced to be used when serializing + /// + /// XML namespace + [PublicAPI] + public XmlSerializer(string @namespace) => Namespace = @namespace; + + /// + /// Serialize the object as XML + /// + /// Object to serialize + /// XML as string + public string Serialize(object obj) { + var doc = new XDocument(); + var t = obj.GetType(); + var name = t.Name; + var options = t.GetAttribute(); + + if (options != null) + name = options.TransformName(options.Name ?? name); + + var root = new XElement(name.AsNamespaced(Namespace)!); + + if (obj is IList list) { + var itemTypeName = ""; + + foreach (var item in list) { + var type = item.GetType(); + var opts = type.GetAttribute(); + + if (opts != null) + itemTypeName = opts.TransformName(opts.Name ?? name); + + if (itemTypeName == "") + itemTypeName = type.Name; + + var instance = new XElement(itemTypeName.AsNamespaced(Namespace)!); + + Map(instance, item); + root.Add(instance); + } + } + else { + Map(root, obj); + } + + if (RootElement != null) { + var wrapper = new XElement(RootElement.AsNamespaced(Namespace)!, root); + doc.Add(wrapper); + } + else { + doc.Add(root); + } + + return doc.ToString(); + } + + /// + /// Name of the root element to use when serializing + /// + public string? RootElement { get; set; } + + /// + /// XML namespace to use when serializing + /// + public string? Namespace { get; set; } + + /// + /// Format string to use when serializing dates + /// + public string? DateFormat { get; set; } + + /// + /// Content type for serialized content + /// + public ContentType ContentType { get; set; } = ContentType.Xml; + + void Map(XContainer root, object obj) { + var objType = obj.GetType(); + + var props = objType.GetProperties() + .Select(p => new { p, indexAttribute = p.GetAttribute() }) + .Where(t => t.p is { CanRead: true, CanWrite: true }) + .OrderBy(t => t.indexAttribute?.Index ?? int.MaxValue) + .Select(t => t.p); + var globalOptions = objType.GetAttribute(); + var textContentAttributeAlreadyUsed = false; + + foreach (var prop in props) { + var name = prop.Name; + var rawValue = prop.GetValue(obj, null); + + if (rawValue == null) + continue; + + var propType = prop.PropertyType; + var useAttribute = false; + var setTextContent = false; + var options = prop.GetAttribute(); + + if (options != null) { + name = options.Name.IsNotEmpty() + ? options.Name + : name; + + name = options.TransformName(name!); + + useAttribute = options.Attribute; + + setTextContent = options.Content; + + if (textContentAttributeAlreadyUsed && setTextContent) + throw new ArgumentException("Class cannot have two properties marked with SerializeAs(Content = true) attribute."); + + textContentAttributeAlreadyUsed |= setTextContent; + } + else if (globalOptions != null) { + name = globalOptions.TransformName(name); + } + + var nsName = name.AsNamespaced(Namespace); + var element = new XElement(nsName!); + + if (propType.GetTypeInfo().IsPrimitive || + propType.GetTypeInfo().IsValueType || + propType == typeof(string)) { + var value = GetSerializedValue(rawValue); + + if (useAttribute) { + root.Add(new XAttribute(name, value)); + continue; + } + + if (setTextContent) { + root.Add(new XText(value)); + continue; + } + + element.Value = value; + } + else if (rawValue is IList items) { + foreach (var item in items) { + var type = item.GetType(); + var setting = type.GetAttribute(); + + var itemTypeName = setting != null && setting.Name.IsNotEmpty() + ? setting.Name + : type.Name; + + var instance = new XElement(itemTypeName.AsNamespaced(Namespace)!); + + Map(instance, item); + + if (setTextContent) { + root.Add(instance); + } + else { + element.Add(instance); + } + } + + if (setTextContent) { + continue; + } + } + else { + Map(element, rawValue); + } + + root.Add(element); + } + } + + string GetSerializedValue(object obj) { + var output = obj switch { + DateTime time when DateFormat.IsNotEmpty() => time.ToString(DateFormat, CultureInfo.InvariantCulture), + bool b => b.ToString().ToLowerInvariant(), + _ => obj + }; + + return IsNumeric(obj) ? SerializeNumber(obj) : output.ToString()!; + } + + static string SerializeNumber(object number) + => number switch { + long l => l.ToString(CultureInfo.InvariantCulture), + ulong @ulong => @ulong.ToString(CultureInfo.InvariantCulture), + int i => i.ToString(CultureInfo.InvariantCulture), + uint u => u.ToString(CultureInfo.InvariantCulture), + decimal @decimal => @decimal.ToString(CultureInfo.InvariantCulture), + float f => f.ToString(CultureInfo.InvariantCulture), + _ => Convert.ToDouble(number, CultureInfo.InvariantCulture).ToString("r", CultureInfo.InvariantCulture) + }; + + /// + /// Determines if a given object is numeric in any way + /// (can be integer, double, null, etc). + /// + static bool IsNumeric(object value) + => value switch { + sbyte => true, + byte => true, + short => true, + ushort => true, + int => true, + uint => true, + long => true, + ulong => true, + float => true, + double => true, + decimal => true, + _ => false + }; +} \ No newline at end of file diff --git a/src/RestSharp.Serializers.Xml/XmlSerializerClientExtensions.cs b/src/RestSharp.Serializers.Xml/XmlSerializerClientExtensions.cs new file mode 100644 index 000000000..72a651a0f --- /dev/null +++ b/src/RestSharp.Serializers.Xml/XmlSerializerClientExtensions.cs @@ -0,0 +1,38 @@ +// 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. + +namespace RestSharp.Serializers.Xml; + +[PublicAPI] +public static class XmlSerializerClientExtensions { + public static SerializerConfig UseXmlSerializer( + this SerializerConfig config, + string? xmlNamespace = null, + string? rootElement = null, + bool useAttributeDeserializer = false + ) { + var xmlSerializer = new XmlSerializer { + Namespace = xmlNamespace, + RootElement = rootElement + }; + + var xmlDeserializer = useAttributeDeserializer ? new XmlAttributeDeserializer() : new XmlDeserializer(); + + var serializer = new XmlRestSerializer() + .WithXmlSerializer(xmlSerializer) + .WithXmlDeserializer(xmlDeserializer); + + return config.UseSerializer(() => serializer); + } +} diff --git a/src/RestSharp/AsyncHelpers.cs b/src/RestSharp/AsyncHelpers.cs new file mode 100644 index 000000000..5d3db9db4 --- /dev/null +++ b/src/RestSharp/AsyncHelpers.cs @@ -0,0 +1,127 @@ +// 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 + +using System.Collections.Concurrent; +using System.Runtime.ExceptionServices; + +namespace RestSharp; + +static class AsyncHelpers { + /// + /// Executes a task synchronously on the calling thread by installing a temporary synchronization context that queues continuations + /// + /// Callback for asynchronous task to run + static void RunSync(Func task) { + var currentContext = SynchronizationContext.Current; + var customContext = new CustomSynchronizationContext(task); + + try { + SynchronizationContext.SetSynchronizationContext(customContext); + customContext.Run(); + } + finally { + SynchronizationContext.SetSynchronizationContext(currentContext); + } + } + + /// + /// Executes a task synchronously on the calling thread by installing a temporary synchronization context that queues continuations + /// + /// Callback for asynchronous task to run + /// Return type for the task + /// Return value from the task + public static T RunSync(Func> task) { + T result = default!; + RunSync(async () => { result = await task(); }); + return result; + } + + /// + /// Synchronization context that can be "pumped" in order to have it execute continuations posted back to it + /// + class CustomSynchronizationContext : SynchronizationContext { + readonly ConcurrentQueue> _items = new(); + readonly AutoResetEvent _workItemsWaiting = new(false); + readonly Func _task; + ExceptionDispatchInfo? _caughtException; + bool _done; + + /// + /// Constructor for the custom context + /// + /// Task to execute + public CustomSynchronizationContext(Func task) => + _task = task ?? throw new ArgumentNullException(nameof(task), "Please remember to pass a Task to be executed"); + + /// + /// When overridden in a derived class, dispatches an asynchronous message to a synchronization context. + /// + /// Callback function + /// Callback state + public override void Post(SendOrPostCallback function, object? state) { + _items.Enqueue(Tuple.Create(function, state)); + _workItemsWaiting.Set(); + } + + /// + /// Enqueues the function to be executed and executes all resulting continuations until it is completely done + /// + public void Run() { + Post(PostCallback, null); + + while (!_done) { + if (_items.TryDequeue(out var task)) { + task.Item1(task.Item2); + if (_caughtException == null) { + continue; + } + _caughtException.Throw(); + } + else { + _workItemsWaiting.WaitOne(); + } + } + + return; + + async void PostCallback(object? _) { + try { + await _task().ConfigureAwait(false); + } + catch (Exception exception) { + _caughtException = ExceptionDispatchInfo.Capture(exception); + throw; + } + finally { + Post(_ => _done = true, null); + } + } + } + + /// + /// When overridden in a derived class, dispatches a synchronous message to a synchronization context. + /// + /// Callback function + /// Callback state + public override void Send(SendOrPostCallback function, object? state) => throw new NotSupportedException("Cannot send to same thread"); + + /// + /// When overridden in a derived class, creates a copy of the synchronization context. Not needed, so just return ourselves. + /// + /// Copy of the context + public override SynchronizationContext CreateCopy() => this; + } +} \ No newline at end of file diff --git a/src/RestSharp/Authenticators/AuthenticatorBase.cs b/src/RestSharp/Authenticators/AuthenticatorBase.cs new file mode 100644 index 000000000..dc765e8c7 --- /dev/null +++ b/src/RestSharp/Authenticators/AuthenticatorBase.cs @@ -0,0 +1,24 @@ +// 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. + +namespace RestSharp.Authenticators; + +public abstract class AuthenticatorBase(string token) : IAuthenticator { + protected string Token { get; set; } = token; + + protected abstract ValueTask GetAuthenticationParameter(string accessToken); + + public async ValueTask Authenticate(IRestClient client, RestRequest request) + => request.AddOrUpdateParameter(await GetAuthenticationParameter(Token).ConfigureAwait(false)); +} \ No newline at end of file diff --git a/src/RestSharp/Authenticators/HttpBasicAuthenticator.cs b/src/RestSharp/Authenticators/HttpBasicAuthenticator.cs new file mode 100644 index 000000000..07d775664 --- /dev/null +++ b/src/RestSharp/Authenticators/HttpBasicAuthenticator.cs @@ -0,0 +1,37 @@ +// 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. + +using System.Text; + +namespace RestSharp.Authenticators; + +/// +/// Allows "basic access authentication" for HTTP requests. +/// +/// +/// Encoding can be specified depending on what your server expect (see https://stackoverflow.com/a/7243567). +/// UTF-8 is used by default but some servers might expect ISO-8859-1 encoding. +/// +[PublicAPI] +public class HttpBasicAuthenticator(string username, string password, Encoding encoding) + : AuthenticatorBase(GetHeader(username, password, encoding)) { + public HttpBasicAuthenticator(string username, string password) : this(username, password, Encoding.UTF8) { } + + static string GetHeader(string username, string password, Encoding encoding) + => Convert.ToBase64String(encoding.GetBytes($"{username}:{password}")); + + // return ; + protected override ValueTask GetAuthenticationParameter(string accessToken) + => new(new HeaderParameter(KnownHeaders.Authorization, $"Basic {accessToken}")); +} \ No newline at end of file diff --git a/RestSharp/Serializers/ISerializer.cs b/src/RestSharp/Authenticators/IAuthenticator.cs similarity index 65% rename from RestSharp/Serializers/ISerializer.cs rename to src/RestSharp/Authenticators/IAuthenticator.cs index 0ca2a4dc3..b7cbe9606 100644 --- a/RestSharp/Serializers/ISerializer.cs +++ b/src/RestSharp/Authenticators/IAuthenticator.cs @@ -1,5 +1,4 @@ -#region License -// Copyright 2010 John Sheehan +// 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. @@ -12,16 +11,9 @@ // 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. -#endregion -namespace RestSharp.Serializers -{ - public interface ISerializer - { - string Serialize(object obj); - string RootElement { get; set; } - string Namespace { get; set; } - string DateFormat { get; set; } - string ContentType { get; set; } - } +namespace RestSharp.Authenticators; + +public interface IAuthenticator { + ValueTask Authenticate(IRestClient client, RestRequest request); } \ No newline at end of file diff --git a/src/RestSharp/Authenticators/JwtAuthenticator.cs b/src/RestSharp/Authenticators/JwtAuthenticator.cs new file mode 100644 index 000000000..cadbd2a64 --- /dev/null +++ b/src/RestSharp/Authenticators/JwtAuthenticator.cs @@ -0,0 +1,34 @@ +// 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. + +namespace RestSharp.Authenticators; + +/// +/// JSON WEB TOKEN (JWT) Authenticator class. +/// https://tools.ietf.org/html/draft-ietf-oauth-json-web-token +/// +public class JwtAuthenticator(string accessToken) : AuthenticatorBase(GetToken(accessToken)) { + /// + /// Set the new bearer token so the request gets the new header value + /// + /// + [PublicAPI] + public void SetBearerToken(string accessToken) => Token = GetToken(accessToken); + + static string GetToken(string accessToken) + => Ensure.NotEmptyString(accessToken, nameof(accessToken)).StartsWith("Bearer ") ? accessToken : $"Bearer {accessToken}"; + + protected override ValueTask GetAuthenticationParameter(string accessToken) + => new(new HeaderParameter(KnownHeaders.Authorization, accessToken)); +} \ No newline at end of file diff --git a/src/RestSharp/Authenticators/OAuth/Enums.cs b/src/RestSharp/Authenticators/OAuth/Enums.cs new file mode 100644 index 000000000..918aad64e --- /dev/null +++ b/src/RestSharp/Authenticators/OAuth/Enums.cs @@ -0,0 +1,23 @@ +// 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. + +namespace RestSharp.Authenticators.OAuth; + +public enum OAuthSignatureMethod { HmacSha1, HmacSha256, PlainText, RsaSha1 } + +public enum OAuthSignatureTreatment { Escaped, Unescaped } + +public enum OAuthParameterHandling { HttpAuthorizationHeader, UrlOrPostParameters } + +public enum OAuthType { RequestToken, AccessToken, ProtectedResource, ClientAuthentication } \ No newline at end of file diff --git a/src/RestSharp/Authenticators/OAuth/Extensions/OAuthExtensions.cs b/src/RestSharp/Authenticators/OAuth/Extensions/OAuthExtensions.cs new file mode 100644 index 000000000..cca257f71 --- /dev/null +++ b/src/RestSharp/Authenticators/OAuth/Extensions/OAuthExtensions.cs @@ -0,0 +1,34 @@ +// 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. + +using System.Security.Cryptography; +using System.Text; + +namespace RestSharp.Authenticators.OAuth.Extensions; + +static class OAuthExtensions { + public static string ToRequestValue(this OAuthSignatureMethod signatureMethod) { + var value = signatureMethod.ToString().ToUpperInvariant(); + var shaIndex = value.IndexOf("SHA", StringComparison.Ordinal); + + return shaIndex > -1 ? value.Insert(shaIndex, "-") : value; + } + + public static string HashWith(this string input, HashAlgorithm algorithm) { + var data = Encoding.UTF8.GetBytes(input); + var hash = algorithm.ComputeHash(data); + + return Convert.ToBase64String(hash); + } +} \ No newline at end of file diff --git a/src/RestSharp/Authenticators/OAuth/Extensions/StringExtensions.cs b/src/RestSharp/Authenticators/OAuth/Extensions/StringExtensions.cs new file mode 100644 index 000000000..3b6d557b0 --- /dev/null +++ b/src/RestSharp/Authenticators/OAuth/Extensions/StringExtensions.cs @@ -0,0 +1,31 @@ +// 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. + +using System.Text; + +namespace RestSharp.Authenticators.OAuth.Extensions; + +static class StringExtensions { + extension(string left) { + public bool EqualsIgnoreCase(string right) => string.Equals(left, right, StringComparison.InvariantCultureIgnoreCase); + + public string Then(string value) => string.Concat(left, value); + + public Uri AsUri() => new(left); + + public byte[] GetBytes() => Encoding.UTF8.GetBytes(left); + + public string PercentEncode() => string.Join("", left.GetBytes().Select(x => $"%{x:X2}")); + } +} \ No newline at end of file diff --git a/src/RestSharp/Authenticators/OAuth/Extensions/TimeExtensions.cs b/src/RestSharp/Authenticators/OAuth/Extensions/TimeExtensions.cs new file mode 100644 index 000000000..8c40f5245 --- /dev/null +++ b/src/RestSharp/Authenticators/OAuth/Extensions/TimeExtensions.cs @@ -0,0 +1,22 @@ +// 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. + +namespace RestSharp.Authenticators.OAuth.Extensions; + +static class TimeExtensions { + public static long ToUnixTime(this DateTime dateTime) { + var timeSpan = dateTime - new DateTime(1970, 1, 1); + return (long)timeSpan.TotalSeconds; + } +} \ No newline at end of file diff --git a/src/RestSharp/Authenticators/OAuth/OAuth1Authenticator.cs b/src/RestSharp/Authenticators/OAuth/OAuth1Authenticator.cs new file mode 100644 index 000000000..f69f5ff39 --- /dev/null +++ b/src/RestSharp/Authenticators/OAuth/OAuth1Authenticator.cs @@ -0,0 +1,313 @@ +// 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. + +using RestSharp.Authenticators.OAuth; +using RestSharp.Extensions; +using System.Web; +// ReSharper disable PropertyCanBeMadeInitOnly.Global + +// ReSharper disable NotResolvedInText +// ReSharper disable CheckNamespace + +namespace RestSharp.Authenticators; + +/// RFC: The OAuth 1.0 Protocol +// ReSharper disable once ClassWithVirtualMembersNeverInherited.Global +public class OAuth1Authenticator : IAuthenticator { + public virtual string? Realm { get; set; } + public virtual OAuthParameterHandling ParameterHandling { get; set; } + public virtual OAuthSignatureMethod SignatureMethod { get; set; } + public virtual OAuthSignatureTreatment SignatureTreatment { get; set; } + public virtual OAuthType Type { get; set; } + public virtual string? ConsumerKey { get; set; } + public virtual string? ConsumerSecret { get; set; } + public virtual string? Token { get; set; } + public virtual string? TokenSecret { get; set; } + public virtual string? Verifier { get; set; } + public virtual string? Version { get; set; } + public virtual string? CallbackUrl { get; set; } + public virtual string? SessionHandle { get; set; } + public virtual string? ClientUsername { get; set; } + public virtual string? ClientPassword { get; set; } + + public ValueTask Authenticate(IRestClient client, RestRequest request) { + var workflow = new OAuthWorkflow { + ConsumerKey = ConsumerKey, + ConsumerSecret = ConsumerSecret, + ParameterHandling = ParameterHandling, + SignatureMethod = SignatureMethod, + SignatureTreatment = SignatureTreatment, + Verifier = Verifier, + Version = Version, + CallbackUrl = CallbackUrl, + SessionHandle = SessionHandle, + Token = Token, + TokenSecret = TokenSecret, + ClientUsername = ClientUsername, + ClientPassword = ClientPassword + }; + + AddOAuthData(client, request, workflow, Type, Realm); + return default; + } + + /// + /// Creates an authenticator to retrieve a request token. + /// + /// Consumer or API key + /// Consumer or API secret + /// Signature method, default is HMAC SHA1 + /// Authenticator instance + [PublicAPI] + public static OAuth1Authenticator ForRequestToken( + string consumerKey, + string? consumerSecret, + OAuthSignatureMethod signatureMethod = OAuthSignatureMethod.HmacSha1 + ) + => new() { + ParameterHandling = OAuthParameterHandling.HttpAuthorizationHeader, + SignatureMethod = signatureMethod, + SignatureTreatment = OAuthSignatureTreatment.Escaped, + ConsumerKey = consumerKey, + ConsumerSecret = consumerSecret, + Type = OAuthType.RequestToken + }; + + /// + /// Creates an authenticator to retrieve a request token with custom callback. + /// + /// Consumer or API key + /// Consumer or API secret + /// URL to where the user will be redirected to after authhentication + /// Authenticator instance + [PublicAPI] + public static OAuth1Authenticator ForRequestToken(string consumerKey, string? consumerSecret, string callbackUrl) { + var authenticator = ForRequestToken(consumerKey, consumerSecret); + + authenticator.CallbackUrl = callbackUrl; + + return authenticator; + } + + /// + /// Creates an authenticator to retrieve an access token using the request token. + /// + /// Consumer or API key + /// Consumer or API secret + /// Request token + /// Request token secret + /// Signature method, default is HMAC SHA1 + /// Authenticator instance + [PublicAPI] + public static OAuth1Authenticator ForAccessToken( + string consumerKey, + string? consumerSecret, + string token, + string tokenSecret, + OAuthSignatureMethod signatureMethod = OAuthSignatureMethod.HmacSha1 + ) + => new() { + ParameterHandling = OAuthParameterHandling.HttpAuthorizationHeader, + SignatureMethod = signatureMethod, + SignatureTreatment = OAuthSignatureTreatment.Escaped, + ConsumerKey = consumerKey, + ConsumerSecret = consumerSecret, + Token = token, + TokenSecret = tokenSecret, + Type = OAuthType.AccessToken + }; + + /// + /// Creates an authenticator to retrieve an access token using the request token and a verifier. + /// + /// Consumer or API key + /// Consumer or API secret + /// Request token + /// Request token secret + /// Verifier received from the API server + /// Authenticator instance + [PublicAPI] + public static OAuth1Authenticator ForAccessToken( + string consumerKey, + string? consumerSecret, + string token, + string tokenSecret, + string verifier + ) { + var authenticator = ForAccessToken(consumerKey, consumerSecret, token, tokenSecret); + + authenticator.Verifier = verifier; + + return authenticator; + } + + [PublicAPI] + public static OAuth1Authenticator ForAccessTokenRefresh( + string consumerKey, + string? consumerSecret, + string token, + string tokenSecret, + string sessionHandle + ) { + var authenticator = ForAccessToken(consumerKey, consumerSecret, token, tokenSecret); + + authenticator.SessionHandle = sessionHandle; + + return authenticator; + } + + [PublicAPI] + public static OAuth1Authenticator ForAccessTokenRefresh( + string consumerKey, + string? consumerSecret, + string token, + string tokenSecret, + string verifier, + string sessionHandle + ) { + var authenticator = ForAccessToken(consumerKey, consumerSecret, token, tokenSecret); + + authenticator.SessionHandle = sessionHandle; + authenticator.Verifier = verifier; + + return authenticator; + } + + [PublicAPI] + public static OAuth1Authenticator ForClientAuthentication( + string consumerKey, + string? consumerSecret, + string username, + string password, + OAuthSignatureMethod signatureMethod = OAuthSignatureMethod.HmacSha1 + ) + => new() { + ParameterHandling = OAuthParameterHandling.HttpAuthorizationHeader, + SignatureMethod = signatureMethod, + SignatureTreatment = OAuthSignatureTreatment.Escaped, + ConsumerKey = consumerKey, + ConsumerSecret = consumerSecret, + ClientUsername = username, + ClientPassword = password, + Type = OAuthType.ClientAuthentication + }; + + /// + /// Creates an authenticator to make calls to protected resources using the access token. + /// + /// Consumer or API key + /// Consumer or API secret + /// Access token + /// Access token secret + /// Signature method, default is HMAC SHA1 + /// Authenticator instance + [PublicAPI] + public static OAuth1Authenticator ForProtectedResource( + string consumerKey, + string? consumerSecret, + string accessToken, + string accessTokenSecret, + OAuthSignatureMethod signatureMethod = OAuthSignatureMethod.HmacSha1 + ) + => new() { + Type = OAuthType.ProtectedResource, + ParameterHandling = OAuthParameterHandling.HttpAuthorizationHeader, + SignatureMethod = signatureMethod, + SignatureTreatment = OAuthSignatureTreatment.Escaped, + ConsumerKey = consumerKey, + ConsumerSecret = consumerSecret, + Token = accessToken, + TokenSecret = accessTokenSecret + }; + + internal static void AddOAuthData( + IRestClient client, + RestRequest request, + OAuthWorkflow workflow, + OAuthType type, + string? realm + ) { + var requestUrl = client.BuildUriWithoutQueryParameters(request).AbsoluteUri; + + if (requestUrl.Contains('?')) + throw new ApplicationException( + "Using query parameters in the base URL is not supported for OAuth calls. Consider using AddDefaultQueryParameter instead." + ); + + var url = client.BuildUri(request).ToString(); + var queryStringStart = url.IndexOf('?'); + + if (queryStringStart != -1) url = url[..queryStringStart]; + + var method = request.Method.ToString().ToUpperInvariant(); + var parameters = new WebPairCollection(); + + var query = + request.AlwaysMultipartFormData || request.Files.Count > 0 + ? x => BaseQuery(x) && x.Name != null && x.Name.StartsWith("oauth_") + : (Func)BaseQuery; + + parameters.AddRange(client.DefaultParameters.Where(query).ToWebParameters()); + parameters.AddRange(request.Parameters.Where(query).ToWebParameters()); + + workflow.RequestUrl = url; + + var oauth = type switch { + OAuthType.RequestToken => workflow.BuildRequestTokenSignature(method, parameters), + OAuthType.AccessToken => workflow.BuildAccessTokenSignature(method, parameters), + OAuthType.ClientAuthentication => workflow.BuildClientAuthAccessTokenSignature(method, parameters), + OAuthType.ProtectedResource => workflow.BuildProtectedResourceSignature(method, parameters), + _ => throw new ArgumentOutOfRangeException(nameof(Type)) + }; + + oauth.Parameters.Add("oauth_signature", oauth.Signature); + + var oauthParameters = workflow.ParameterHandling switch { + OAuthParameterHandling.HttpAuthorizationHeader => CreateHeaderParameters(), + OAuthParameterHandling.UrlOrPostParameters => CreateUrlParameters(), + _ => throw new ArgumentOutOfRangeException(nameof(ParameterHandling)) + }; + + request.AddOrUpdateParameters(oauthParameters); + return; + + // include all GET and POST parameters before generating the signature + // according to the RFC 5849 - The OAuth 1.0 Protocol + // http://tools.ietf.org/html/rfc5849#section-3.4.1 + // if this change causes trouble we need to introduce a flag indicating the specific OAuth implementation level, + // or implement a separate class for each OAuth version + static bool BaseQuery(Parameter x) => x.Type is ParameterType.GetOrPost or ParameterType.QueryString; + + IEnumerable CreateHeaderParameters() => [new HeaderParameter(KnownHeaders.Authorization, GetAuthorizationHeader())]; + + IEnumerable CreateUrlParameters() => oauth.Parameters.Select(p => new GetOrPostParameter(p.Name, HttpUtility.UrlDecode(p.Value))); + + string GetAuthorizationHeader() { + var oathParameters = + oauth.Parameters + .OrderBy(x => x, WebPair.Comparer) + .Select(x => x.GetQueryParameter(true)) + .ToList(); + + if (!realm.IsEmpty()) oathParameters.Insert(0, $"realm=\"{OAuthTools.UrlEncodeRelaxed(realm)}\""); + + return $"OAuth {string.Join(",", oathParameters)}"; + } + } +} + +static class ParametersExtensions { + internal static IEnumerable ToWebParameters(this IEnumerable p) + => p.Select(x => new WebPair(Ensure.NotNull(x.Name, "Parameter name"), x.Value?.ToString())); +} \ No newline at end of file diff --git a/src/RestSharp/Authenticators/OAuth/OAuthTools.cs b/src/RestSharp/Authenticators/OAuth/OAuthTools.cs new file mode 100644 index 000000000..8b43f1dd0 --- /dev/null +++ b/src/RestSharp/Authenticators/OAuth/OAuthTools.cs @@ -0,0 +1,262 @@ +// 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. + +using System.Diagnostics.CodeAnalysis; +using System.Security.Cryptography; +using System.Text; +using RestSharp.Authenticators.OAuth.Extensions; +using RestSharp.Extensions; +using static RestSharp.Authenticators.OAuth.OAuthSignatureMethod; + +namespace RestSharp.Authenticators.OAuth; + +static class OAuthTools { + const string AlphaNumeric = Upper + Lower + Digit; + const string Digit = "1234567890"; + const string Lower = "abcdefghijklmnopqrstuvwxyz"; + const string Unreserved = AlphaNumeric + "-._~"; + const string Upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + + static readonly Random Random; + static readonly object RandomLock = new(); + static readonly RandomNumberGenerator Rng = RandomNumberGenerator.Create(); + static readonly Encoding Encoding = Encoding.UTF8; + + /// + /// The set of characters that are unreserved in RFC 2396 but are NOT unreserved in RFC 3986. + /// + static readonly string[] UriRfc3986CharsToEscape = ["!", "*", "'", "(", ")"]; + + static readonly string[] UriRfc3968EscapedHex = ["%21", "%2A", "%27", "%28", "%29"]; + + static OAuthTools() { + var bytes = new byte[4]; + + Rng.GetBytes(bytes); + Random = new(BitConverter.ToInt32(bytes, 0)); + } + + /// + /// Generates a random 16-byte lowercase alphanumeric string. + /// + /// + public static string GetNonce() { + const string chars = Lower + Digit; + + var nonce = new char[16]; + + lock (RandomLock) { + for (var i = 0; i < nonce.Length; i++) nonce[i] = chars[Random.Next(0, chars.Length)]; + } + + return new(nonce); + } + + /// + /// Generates a timestamp based on the current elapsed seconds since '01/01/1970 0000 GMT" + /// + /// + public static string GetTimestamp() => GetTimestamp(DateTime.UtcNow); + + /// + /// Generates a timestamp based on the elapsed seconds of a given time since '01/01/1970 0000 GMT" + /// + /// A specified point in time. + /// + static string GetTimestamp(DateTime dateTime) => dateTime.ToUnixTime().ToString(); + + /// + /// URL encodes a string based on section 5.1 of the OAuth spec. + /// Namely, percent encoding with [RFC3986], avoiding unreserved characters, + /// upper-casing hexadecimal characters, and UTF-8 encoding for text value pairs. + /// + /// The value to escape. + /// The escaped value. + [return: NotNullIfNotNull(nameof(value))] + public static string? UrlEncodeRelaxed(string? value) { + if (value == null) return null; + + // Do RFC 2396 escaping by calling the .NET method to do the work. + var escaped = Uri.EscapeDataString(value); + + // Escape RFC 3986 chars first. + var escapedRfc3986 = new StringBuilder(escaped); + + for (var i = 0; i < UriRfc3986CharsToEscape.Length; i++) { + var t = UriRfc3986CharsToEscape[i]; + + escapedRfc3986.Replace(t, UriRfc3968EscapedHex[i]); + } + + // Return the fully-RFC3986-escaped string. + return escapedRfc3986.ToString(); + } + + /// + /// URL encodes a string based on section 5.1 of the OAuth spec. + /// Namely, percent encoding with [RFC3986], avoiding unreserved characters, + /// upper-casing hexadecimal characters, and UTF-8 encoding for text value pairs. + /// + /// + // From oauth spec above: - + // Characters not in the unreserved character set ([RFC3986] + // (Berners-Lee, T., "Uniform Resource Identifiers (URI): + // Generic Syntax," .) section 2.3) MUST be encoded. + // ... + // unreserved = ALPHA, DIGIT, '-', '.', '_', '~' + [return: NotNullIfNotNull(nameof(value))] + public static string? UrlEncodeStrict(string? value) + => value == null ? null : string.Join("", value.Select(x => Unreserved.Contains(x) ? x.ToString() : $"%{(byte)x:X2}")); + + /// + /// Sorts a collection of key-value pairs by name, and then value if equal, + /// concatenating them into a single string. This string should be encoded + /// prior to, or after normalization is run. + /// + /// + /// + internal static string NormalizeRequestParameters(WebPairCollection parameters) => string.Join("&", SortParametersExcludingSignature(parameters)); + + /// + /// Sorts a by name, and then value if equal. + /// + /// A collection of parameters to sort + /// A sorted parameter collection + internal static IEnumerable SortParametersExcludingSignature(WebPairCollection parameters) + => parameters + .Where(x => !x.Name.EqualsIgnoreCase("oauth_signature")) + .Select(x => new WebPair(UrlEncodeStrict(x.Name), UrlEncodeRelaxed(x.Value))) + .OrderBy(x => x, WebPair.Comparer) + .Select(x => x.GetQueryParameter(false)); + + /// + /// Creates a request URL suitable for making OAuth requests. + /// Resulting URLs must exclude port 80 or port 443 when accompanied by HTTP and HTTPS, respectively. + /// Resulting URLs must be lower case. + /// + /// The original request URL + /// + static string ConstructRequestUrl(Uri url) { + Ensure.NotNull(url, nameof(url)); + + var basic = url is { Scheme: "http", Port : 80 }; + var secure = url is { Scheme: "https", Port: 443 }; + var port = basic || secure ? "" : $":{url.Port}"; + + // Decode the path to avoid double-encoding when the path contains already-encoded characters. + // For example, if a URL segment was added with AddUrlSegment("id", "value!"), it gets encoded + // to "value%21" in the URL. When we extract url.AbsolutePath, it contains "%21" (encoded). + // If we then call UrlEncodeRelaxed on it, Uri.EscapeDataString would encode the "%" to "%25", + // resulting in "%2521" (double-encoded). By decoding first, we ensure proper single encoding. + // + // Security note: This is safe because: + // - The url parameter is a validated Uri object constructed by RestSharp's BuildUri() + // - The decoded path is immediately re-encoded by UrlEncodeRelaxed before use + // - There is no direct user input involved in this internal OAuth signature calculation + var decodedPath = Uri.UnescapeDataString(url.AbsolutePath); + + return $"{url.Scheme}://{url.Host}{port}{decodedPath}"; + } + + /// + /// Creates a request elements concatenation value to send with a request. + /// This is also known as the signature base. + /// + /// The request HTTP method type + /// The request URL + /// The request parameters + /// A signature base string + public static string ConcatenateRequestElements(string method, string url, WebPairCollection parameters) { + // Separating &'s are not URL encoded + var requestMethod = method.ToUpperInvariant().Then("&"); + var requestUrl = UrlEncodeRelaxed(ConstructRequestUrl(url.AsUri())).Then("&"); + var requestParameters = UrlEncodeRelaxed(NormalizeRequestParameters(parameters)); + + return $"{requestMethod}{requestUrl}{requestParameters}"; + } + + /// + /// Creates a signature value given a signature base and the consumer secret. + /// This method is used when the token secret is currently unknown. + /// + /// The hashing method + /// The signature base + /// The consumer key + /// + public static string GetSignature( + OAuthSignatureMethod signatureMethod, + string signatureBase, + string? consumerSecret + ) + => GetSignature(signatureMethod, OAuthSignatureTreatment.Escaped, signatureBase, consumerSecret); + + /// + /// Creates a signature value given a signature base and the consumer secret and a known token secret. + /// + /// The hashing method + /// The treatment to use on a signature value + /// The signature base + /// The consumer secret + /// The token secret + /// + public static string GetSignature( + OAuthSignatureMethod signatureMethod, + OAuthSignatureTreatment signatureTreatment, + string signatureBase, + string? consumerSecret, + string? tokenSecret = null + ) { + if (tokenSecret.IsEmpty()) tokenSecret = string.Empty; + if (consumerSecret.IsEmpty()) consumerSecret = string.Empty; + + var unencodedConsumerSecret = consumerSecret; + consumerSecret = Uri.EscapeDataString(consumerSecret); + tokenSecret = Uri.EscapeDataString(tokenSecret); + + var signature = signatureMethod switch { + HmacSha1 => GetHmacSignature(new HMACSHA1(), consumerSecret, tokenSecret, signatureBase), + HmacSha256 => GetHmacSignature(new HMACSHA256(), consumerSecret, tokenSecret, signatureBase), + RsaSha1 => GetRsaSignature(), + PlainText => $"{consumerSecret}&{tokenSecret}", + _ => throw new NotImplementedException("Only HMAC-SHA1, HMAC-SHA256, and RSA-SHA1 are currently supported.") + }; + + var result = signatureTreatment == OAuthSignatureTreatment.Escaped + ? UrlEncodeRelaxed(signature) + : signature; + + return result; + + string GetRsaSignature() { + using var provider = new RSACryptoServiceProvider(); + provider.PersistKeyInCsp = false; + + provider.FromXmlString(unencodedConsumerSecret); + +#if NET + var hash = SHA1.HashData(Encoding.GetBytes(signatureBase)); +#else + var hasher = SHA1.Create(); + var hash = hasher.ComputeHash(Encoding.GetBytes(signatureBase)); +#endif + return Convert.ToBase64String(provider.SignHash(hash, CryptoConfig.MapNameToOID("SHA1"))); + } + } + + static string GetHmacSignature(KeyedHashAlgorithm crypto, string consumerSecret, string tokenSecret, string signatureBase) { + var key = $"{consumerSecret}&{tokenSecret}"; + crypto.Key = Encoding.GetBytes(key); + return signatureBase.HashWith(crypto); + } +} diff --git a/src/RestSharp/Authenticators/OAuth/OAuthWorkflow.cs b/src/RestSharp/Authenticators/OAuth/OAuthWorkflow.cs new file mode 100644 index 000000000..d1818ec55 --- /dev/null +++ b/src/RestSharp/Authenticators/OAuth/OAuthWorkflow.cs @@ -0,0 +1,184 @@ +// 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. + +using System.Web; +using RestSharp.Authenticators.OAuth.Extensions; + +namespace RestSharp.Authenticators.OAuth; + +/// +/// A class to encapsulate OAuth authentication flow. +/// +sealed class OAuthWorkflow { + public string? Version { get; init; } + public string? ConsumerKey { get; init; } + public string? ConsumerSecret { get; init; } + public string? Token { get; init; } + public string? TokenSecret { get; init; } + public string? CallbackUrl { get; init; } + public string? Verifier { get; init; } + public string? SessionHandle { get; init; } + public OAuthSignatureMethod SignatureMethod { get; init; } + public OAuthSignatureTreatment SignatureTreatment { get; init; } + public OAuthParameterHandling ParameterHandling { get; set; } + public string? ClientUsername { get; init; } + public string? ClientPassword { get; init; } + public string? RequestUrl { get; set; } + + internal Func GetTimestamp { get; init; } = OAuthTools.GetTimestamp; + internal Func GetNonce { get; init; } = OAuthTools.GetNonce; + + /// + /// Generates an OAuth signature to pass to an + /// for the purpose of requesting an + /// unauthorized request token. + /// + /// The HTTP method for the intended request + /// Any existing, non-OAuth query parameters desired in the request + /// + public OAuthParameters BuildRequestTokenSignature(string method, WebPairCollection parameters) { + Ensure.NotEmptyString(ConsumerKey, nameof(ConsumerKey)); + + var allParameters = new WebPairCollection(); + allParameters.AddRange(parameters); + + var uri = new Uri(Ensure.NotEmptyString(RequestUrl, nameof(RequestUrl))); + var timestamp = GetTimestamp(); + var nonce = GetNonce(); + + var authParameters = GenerateAuthParameters(timestamp, nonce); + allParameters.AddRange(authParameters); + + var signatureBase = OAuthTools.ConcatenateRequestElements(method, uri.ToString(), allParameters); + + return new() { + Signature = OAuthTools.GetSignature(SignatureMethod, SignatureTreatment, signatureBase, ConsumerSecret), + Parameters = authParameters + }; + } + + /// + /// Generates an OAuth signature to pass to the + /// for the purpose of exchanging a request token + /// for an access token authorized by the user at the Service Provider site. + /// + /// The HTTP method for the intended request + /// Any existing, non-OAuth query parameters desired in the request + public OAuthParameters BuildAccessTokenSignature(string method, WebPairCollection parameters) { + Ensure.NotEmptyString(ConsumerKey, nameof(ConsumerKey)); + Ensure.NotEmptyString(Token, nameof(Token)); + + var allParameters = new WebPairCollection(); + allParameters.AddRange(parameters); + + var uri = new Uri(Ensure.NotEmptyString(RequestUrl, nameof(RequestUrl))); + var timestamp = GetTimestamp(); + var nonce = GetNonce(); + + var authParameters = GenerateAuthParameters(timestamp, nonce); + allParameters.AddRange(authParameters); + + var signatureBase = OAuthTools.ConcatenateRequestElements(method, uri.ToString(), allParameters); + + return new() { + Signature = OAuthTools.GetSignature(SignatureMethod, SignatureTreatment, signatureBase, ConsumerSecret, TokenSecret), + Parameters = authParameters + }; + } + + /// + /// Generates an OAuth signature to pass to an + /// for the purpose of exchanging user credentials + /// for an access token authorized by the user at the Service Provider site. + /// + /// The HTTP method for the intended request + /// Any existing, non-OAuth query parameters desired in the request + public OAuthParameters BuildClientAuthAccessTokenSignature(string method, WebPairCollection parameters) { + Ensure.NotEmptyString(ConsumerKey, nameof(ConsumerKey)); + Ensure.NotEmptyString(ClientUsername, nameof(ClientUsername)); + + var allParameters = new WebPairCollection(); + allParameters.AddRange(parameters); + + var uri = new Uri(Ensure.NotEmptyString(RequestUrl, nameof(RequestUrl))); + var timestamp = GetTimestamp(); + var nonce = GetNonce(); + + var authParameters = GenerateXAuthParameters(timestamp, nonce); + allParameters.AddRange(authParameters); + + var signatureBase = OAuthTools.ConcatenateRequestElements(method, uri.ToString(), allParameters); + + return new() { + Signature = OAuthTools.GetSignature(SignatureMethod, SignatureTreatment, signatureBase, ConsumerSecret), + Parameters = authParameters + }; + } + + public OAuthParameters BuildProtectedResourceSignature(string method, WebPairCollection parameters) { + Ensure.NotEmptyString(ConsumerKey, nameof(ConsumerKey)); + + var allParameters = new WebPairCollection(); + allParameters.AddRange(parameters); + + // Include url parameters in query pool + var uri = new Uri(Ensure.NotEmptyString(RequestUrl, nameof(RequestUrl))); + var urlParameters = HttpUtility.ParseQueryString(uri.Query); + + allParameters.AddRange(urlParameters.AllKeys.Select(x => new WebPair(x!, urlParameters[x]!))); + + var timestamp = GetTimestamp(); + var nonce = GetNonce(); + + var authParameters = GenerateAuthParameters(timestamp, nonce); + allParameters.AddRange(authParameters); + + var signatureBase = OAuthTools.ConcatenateRequestElements(method, uri.ToString(), allParameters); + + return new() { + Signature = OAuthTools.GetSignature(SignatureMethod, SignatureTreatment, signatureBase, ConsumerSecret, TokenSecret), + Parameters = authParameters + }; + } + + WebPairCollection GenerateAuthParameters(string timestamp, string nonce) + => new WebPairCollection { + new("oauth_consumer_key", Ensure.NotNull(ConsumerKey, nameof(ConsumerKey)), true), + new("oauth_nonce", nonce), + new("oauth_signature_method", SignatureMethod.ToRequestValue()), + new("oauth_timestamp", timestamp), + new("oauth_version", Version ?? "1.0") + } + .AddNotEmpty("oauth_token", Token, true) + .AddNotEmpty("oauth_callback", CallbackUrl, true) + .AddNotEmpty("oauth_verifier", Verifier) + .AddNotEmpty("oauth_session_handle", SessionHandle); + + WebPairCollection GenerateXAuthParameters(string timestamp, string nonce) + => [ + new("x_auth_username", Ensure.NotNull(ClientUsername, nameof(ClientUsername))), + new("x_auth_password", Ensure.NotNull(ClientPassword, nameof(ClientPassword))), + new("x_auth_mode", "client_auth"), + new("oauth_consumer_key", Ensure.NotNull(ConsumerKey, nameof(ConsumerKey)), true), + new("oauth_signature_method", SignatureMethod.ToRequestValue()), + new("oauth_timestamp", timestamp), + new("oauth_nonce", nonce), + new("oauth_version", Version ?? "1.0") + ]; + + internal class OAuthParameters { + public WebPairCollection Parameters { get; init; } = null!; + public string Signature { get; init; } = null!; + } +} \ No newline at end of file diff --git a/src/RestSharp/Authenticators/OAuth/WebPair.cs b/src/RestSharp/Authenticators/OAuth/WebPair.cs new file mode 100644 index 000000000..381fbbe24 --- /dev/null +++ b/src/RestSharp/Authenticators/OAuth/WebPair.cs @@ -0,0 +1,36 @@ +// 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. + +namespace RestSharp.Authenticators.OAuth; + +class WebPair(string name, string? value, bool encode = false) { + public string Name { get; } = name; + public string? Value { get; } = value; + string? WebValue { get; } = encode ? OAuthTools.UrlEncodeRelaxed(value) : value; + + public string GetQueryParameter(bool web) { + var value = web ? $"\"{WebValue}\"" : Value; + return value == null ? Name : $"{Name}={value}"; + } + + internal static WebPairComparer Comparer { get; } = new(); + + internal class WebPairComparer : IComparer { + public int Compare(WebPair? x, WebPair? y) { + var compareName = string.CompareOrdinal(x?.Name, y?.Name); + + return compareName != 0 ? compareName : string.CompareOrdinal(x?.Value, y?.Value); + } + } +} diff --git a/src/RestSharp/Authenticators/OAuth/WebPairCollection.cs b/src/RestSharp/Authenticators/OAuth/WebPairCollection.cs new file mode 100644 index 000000000..550d0e7d6 --- /dev/null +++ b/src/RestSharp/Authenticators/OAuth/WebPairCollection.cs @@ -0,0 +1,63 @@ +// 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. + +using System.Collections; + +namespace RestSharp.Authenticators.OAuth; + +class WebPairCollection : IList { + readonly List _parameters = []; + + public IEnumerator GetEnumerator() => _parameters.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public void Add(WebPair parameter) => _parameters.Add(parameter); + + public void AddRange(IEnumerable collection) => AddCollection(collection); + + public void Add(string name, string value) => Add(new(name, value)); + + public WebPairCollection AddNotEmpty(string name, string? value, bool encode = false) { + if (value != null) + Add(new(name, value, encode)); + return this; + } + + public void Clear() => _parameters.Clear(); + + public bool Contains(WebPair parameter) => _parameters.Contains(parameter); + + public void CopyTo(WebPair[] parametersArray, int arrayIndex) => _parameters.CopyTo(parametersArray, arrayIndex); + + public bool Remove(WebPair parameter) => _parameters.Remove(parameter); + + public int Count => _parameters.Count; + + public bool IsReadOnly => false; + + public int IndexOf(WebPair parameter) => _parameters.IndexOf(parameter); + + public void Insert(int index, WebPair parameter) => _parameters.Insert(index, parameter); + + public void RemoveAt(int index) => _parameters.RemoveAt(index); + + public WebPair this[int index] { + get => _parameters[index]; + set => _parameters[index] = value; + } + + void AddCollection(IEnumerable collection) + => _parameters.AddRange(collection.Select(parameter => new WebPair(parameter.Name, parameter.Value))); +} \ No newline at end of file diff --git a/src/RestSharp/Authenticators/OAuth2/OAuth2AuthorizationRequestHeaderAuthenticator.cs b/src/RestSharp/Authenticators/OAuth2/OAuth2AuthorizationRequestHeaderAuthenticator.cs new file mode 100644 index 000000000..55a6bf5d5 --- /dev/null +++ b/src/RestSharp/Authenticators/OAuth2/OAuth2AuthorizationRequestHeaderAuthenticator.cs @@ -0,0 +1,36 @@ +// 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. + +namespace RestSharp.Authenticators.OAuth2; + +/// +/// The OAuth 2 authenticator using the authorization request header field. +/// +/// +/// Based on http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-5.1.1 +/// +[PublicAPI] +public class OAuth2AuthorizationRequestHeaderAuthenticator : AuthenticatorBase { + readonly string _tokenType; + + /// + /// Initializes a new instance of the class. + /// + /// The access token. + /// The token type. + public OAuth2AuthorizationRequestHeaderAuthenticator(string accessToken, string tokenType = "OAuth") : base(accessToken) => _tokenType = tokenType; + + protected override ValueTask GetAuthenticationParameter(string accessToken) + => new(new HeaderParameter(KnownHeaders.Authorization, $"{_tokenType} {accessToken}")); +} \ No newline at end of file diff --git a/src/RestSharp/Authenticators/OAuth2/OAuth2UriQueryParameterAuthenticator.cs b/src/RestSharp/Authenticators/OAuth2/OAuth2UriQueryParameterAuthenticator.cs new file mode 100644 index 000000000..9ea1b593f --- /dev/null +++ b/src/RestSharp/Authenticators/OAuth2/OAuth2UriQueryParameterAuthenticator.cs @@ -0,0 +1,33 @@ +// 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. + +namespace RestSharp.Authenticators.OAuth2; + +/// +/// The OAuth 2 authenticator using URI query parameter. +/// +/// +/// Based on http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-5.1.2 +/// +[PublicAPI] +public class OAuth2UriQueryParameterAuthenticator : AuthenticatorBase { + /// + /// Initializes a new instance of the class. + /// + /// The access token. + public OAuth2UriQueryParameterAuthenticator(string accessToken) : base(accessToken) { } + + protected override ValueTask GetAuthenticationParameter(string accessToken) + => new(new GetOrPostParameter("oauth_token", accessToken)); +} \ No newline at end of file diff --git a/src/RestSharp/BuildUriExtensions.cs b/src/RestSharp/BuildUriExtensions.cs new file mode 100644 index 000000000..6bf976244 --- /dev/null +++ b/src/RestSharp/BuildUriExtensions.cs @@ -0,0 +1,93 @@ +// 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. +// + +namespace RestSharp; + +public static class BuildUriExtensions { + /// Client instance + extension(IRestClient client) { + /// + /// Builds the URI for the request + /// + /// Request instance + /// + public Uri BuildUri(RestRequest request) { + DoBuildUriValidations(client, request); + + var (uri, resource) = client.Options.BaseUrl.GetUrlSegmentParamsValues( + request.Resource, + client.Options.Encode, + request.Parameters, + client.DefaultParameters + ); + var mergedUri = uri.MergeBaseUrlAndResource(resource); + var query = client.GetRequestQuery(request); + return mergedUri.AddQueryString(query); + } + + /// + /// Builds the URI for the request without query parameters. + /// + /// Request instance + /// + public Uri BuildUriWithoutQueryParameters(RestRequest request) { + DoBuildUriValidations(client, request); + + var (uri, resource) = client.Options.BaseUrl.GetUrlSegmentParamsValues( + request.Resource, + client.Options.Encode, + request.Parameters, + client.DefaultParameters + ); + return uri.MergeBaseUrlAndResource(resource); + } + + /// + /// Gets the query string for the request. + /// + /// Request instance + /// + [PublicAPI] + public string? GetRequestQuery(RestRequest request) { + var parametersCollections = new ParametersCollection[] { request.Parameters, client.DefaultParameters }; + + var parameters = parametersCollections.SelectMany(x => x.GetQueryParameters(request.Method)).ToList(); + + return parameters.Count == 0 ? null : string.Join("&", parameters.Select(EncodeParameter).ToArray()); + + string GetString(string name, string? value, Func? encode) { + var val = encode != null && value != null ? encode(value) : value; + return val == null ? name : $"{name}={val}"; + } + + string EncodeParameter(Parameter parameter) + => !parameter.Encode + ? GetString(parameter.Name!, parameter.Value?.ToString(), null) + : GetString( + client.Options.EncodeQuery(parameter.Name!, client.Options.Encoding), + parameter.Value?.ToString(), + x => client.Options.EncodeQuery(x, client.Options.Encoding) + ); + } + } + + static void DoBuildUriValidations(IRestClient client, RestRequest request) { + if (client.Options.BaseUrl == null && !request.Resource.StartsWith("http", StringComparison.InvariantCultureIgnoreCase)) + throw new ArgumentOutOfRangeException( + nameof(request), + "Request resource doesn't contain a valid scheme for an empty base URL of the client" + ); + } +} diff --git a/src/RestSharp/ContentType.cs b/src/RestSharp/ContentType.cs new file mode 100644 index 000000000..8f8d70554 --- /dev/null +++ b/src/RestSharp/ContentType.cs @@ -0,0 +1,84 @@ +// 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. + +using System.Net.Http.Headers; + +namespace RestSharp; + +public delegate bool SupportsContentType(ContentType contentType); + +public class ContentType : IEquatable { + ContentType(string contentType) { + var ct = Ensure.NotEmptyString(contentType, nameof(contentType)); + if (!ct.Contains('/')) throw new ArgumentException("Invalid content type string", nameof(contentType)); + + _value = ct; + } + + public static readonly ContentType Json = "application/json"; + public static readonly ContentType Xml = "application/xml"; + public static readonly ContentType Plain = "text/plain"; + public static readonly ContentType Csv = "text/csv"; + public static readonly ContentType Binary = "application/octet-stream"; + public static readonly ContentType GZip = "application/x-gzip"; + public static readonly ContentType FormUrlEncoded = "application/x-www-form-urlencoded"; + public static readonly ContentType Undefined = "undefined/undefined"; + + public string Value => _value == Undefined._value ? Plain._value : _value; + + public static ContentType FromDataFormat(DataFormat dataFormat) => DataFormatMap[dataFormat]; + + public override string ToString() => Value; + + public static implicit operator ContentType(string? contentType) => contentType == null ? Undefined : new(contentType); + + public static implicit operator string(ContentType contentType) => contentType.Value; + + public ContentType Or(ContentType? contentType) => Equals(Undefined) ? contentType ?? Plain : this; + + public string OrValue(string? contentType) => Equals(Undefined) ? contentType ?? Plain.Value : Value; + + public MediaTypeHeaderValue AsMediaTypeHeaderValue => MediaTypeHeaderValue.Parse(Value); + + static readonly Dictionary DataFormatMap = + new() { + { DataFormat.Xml, Xml }, + { DataFormat.Json, Json }, + { DataFormat.None, Plain }, + { DataFormat.Binary, Binary } + }; + + public static readonly string[] JsonAccept = [Json, "text/json", "text/x-json", "text/javascript"]; + + public static readonly string[] XmlAccept = [Xml, "text/xml"]; + + readonly string _value; + + public bool Equals(ContentType? other) { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + + return _value == other._value; + } + + public override bool Equals(object? obj) { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + + return Equals((ContentType)obj); + } + + public override int GetHashCode() => _value.GetHashCode(); +} \ No newline at end of file diff --git a/src/RestSharp/Ensure.cs b/src/RestSharp/Ensure.cs new file mode 100644 index 000000000..dbdb56f9d --- /dev/null +++ b/src/RestSharp/Ensure.cs @@ -0,0 +1,26 @@ +// 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. + +namespace RestSharp; + +static class Ensure { + public static T NotNull(T? value, [InvokerParameterName] string name) => value ?? throw new ArgumentNullException(name); + + public static string NotEmptyString(object? value, [InvokerParameterName] string name) { + var s = value as string ?? value?.ToString(); + if (s == null) throw new ArgumentNullException(name); + + return string.IsNullOrWhiteSpace(s) ? throw new ArgumentException("Parameter cannot be an empty string", name) : s; + } +} \ No newline at end of file diff --git a/src/RestSharp/Enum.cs b/src/RestSharp/Enum.cs new file mode 100644 index 000000000..c6ebf0cfb --- /dev/null +++ b/src/RestSharp/Enum.cs @@ -0,0 +1,118 @@ +// 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. + +namespace RestSharp; + +/// +/// Types of parameters that can be added to requests +/// +public enum ParameterType { + /// + /// A that will added to the QueryString for GET, DELETE, OPTIONS and HEAD requests; and form for POST and PUT requests. + /// + /// + /// See . + /// + GetOrPost, + + /// + /// A that will be added to part of the url by replacing a {placeholder} within the absolute path. + /// + /// + /// See . + /// + UrlSegment, + + /// + /// A that will be added as a request header + /// + /// + /// See . + /// + HttpHeader, + + /// + /// A that will be added to the request body + /// + /// + /// See . + /// + RequestBody, + + /// + /// A that will be added to the query string + /// + /// + /// See . + /// + QueryString +} + +/// +/// Data formats +/// +public enum DataFormat { Json, Xml, Binary, None } + +/// +/// HTTP method to use when making requests +/// +public enum Method { + Get, Post, Put, Delete, Head, Options, + Patch, Merge, Copy, Search +} + +/// +/// Format strings for commonly-used date formats +/// +public struct DateFormat { + /// + /// .NET format string for ISO 8601 date format + /// + public const string ISO_8601 = "s"; + + /// + /// .NET format string for roundtrip date format + /// + public const string ROUND_TRIP = "u"; +} + +/// +/// Status for responses (surprised?) +/// +public enum ResponseStatus { + /// + /// Not Applicable, for when the Request has not yet been made + /// + None, + + /// + /// for when the request is passes as a result of being true, or when the response is + /// + Completed, + + /// + /// for when the request fails due as a result of being false except for the case when the response is + /// + Error, + + /// + /// for when the Operation is cancelled due to the request taking longer than the length of time prescribed by or due to the timing out. + /// + TimedOut, + + /// + /// for when the Operation is cancelled, due to reasons other than + /// + Aborted +} \ No newline at end of file diff --git a/src/RestSharp/Extensions/CollectionExtensions.cs b/src/RestSharp/Extensions/CollectionExtensions.cs new file mode 100644 index 000000000..7d04f99a9 --- /dev/null +++ b/src/RestSharp/Extensions/CollectionExtensions.cs @@ -0,0 +1,21 @@ +// 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. + +namespace RestSharp.Extensions; + +static class CollectionExtensions { + public static void ForEach(this IEnumerable items, Action action) { + foreach (var item in items) action(item); + } +} \ No newline at end of file diff --git a/src/RestSharp/Extensions/CookieContainerExtensions.cs b/src/RestSharp/Extensions/CookieContainerExtensions.cs new file mode 100644 index 000000000..8df9142d7 --- /dev/null +++ b/src/RestSharp/Extensions/CookieContainerExtensions.cs @@ -0,0 +1,29 @@ +// 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. +// + +namespace RestSharp.Extensions; + +static class CookieContainerExtensions { + public static void AddCookies(this CookieContainer cookieContainer, Uri uri, IEnumerable cookiesHeader) { + foreach (var header in cookiesHeader) { + try { + cookieContainer.SetCookies(uri, header); + } + catch (CookieException) { + // Do not fail request if we cannot parse a cookie + } + } + } +} diff --git a/src/RestSharp/Extensions/GenerateImmutableAttribute.cs b/src/RestSharp/Extensions/GenerateImmutableAttribute.cs new file mode 100644 index 000000000..e049e72e9 --- /dev/null +++ b/src/RestSharp/Extensions/GenerateImmutableAttribute.cs @@ -0,0 +1,29 @@ +// 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. +// + +namespace RestSharp.Extensions; + +[AttributeUsage(AttributeTargets.Class)] +class GenerateImmutableAttribute : Attribute; + +[AttributeUsage(AttributeTargets.Class)] +class GenerateCloneAttribute : Attribute { + public Type? BaseType { get; set; } + public string? Name { get; set; } + public bool Mutate { get; set; } +}; + +[AttributeUsage(AttributeTargets.Property)] +class Exclude : Attribute; \ No newline at end of file diff --git a/src/RestSharp/Extensions/HttpHeadersExtensions.cs b/src/RestSharp/Extensions/HttpHeadersExtensions.cs new file mode 100644 index 000000000..9dabe4755 --- /dev/null +++ b/src/RestSharp/Extensions/HttpHeadersExtensions.cs @@ -0,0 +1,26 @@ +// 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. +// + +using System.Net.Http.Headers; + +namespace RestSharp.Extensions; + +static class HttpHeadersExtensions { + public static IReadOnlyCollection GetHeaderParameters(this HttpHeaders httpHeaders) + => httpHeaders + .SelectMany(x => x.Value.Select(y => (x.Key, y))) + .Select(x => new HeaderParameter(x.Key, x.y)) + .ToList(); +} \ No newline at end of file diff --git a/src/RestSharp/Extensions/HttpResponseExtensions.cs b/src/RestSharp/Extensions/HttpResponseExtensions.cs new file mode 100644 index 000000000..8cfd52a60 --- /dev/null +++ b/src/RestSharp/Extensions/HttpResponseExtensions.cs @@ -0,0 +1,71 @@ +// 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. +// + +using System.Text; + +namespace RestSharp.Extensions; + +static class HttpResponseExtensions { + 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); +#else + : new HttpRequestException($"Request failed with status code {httpResponse.StatusCode}"); +#endif + + public static async Task GetResponseString(this HttpResponseMessage response, byte[] bytes, Encoding clientEncoding) { + var encodingString = response.Content.Headers.ContentType?.CharSet; + var encoding = encodingString != null ? TryGetEncoding(encodingString) : clientEncoding; + + using var reader = new StreamReader(new MemoryStream(bytes), encoding); + return await reader.ReadToEndAsync(); + Encoding TryGetEncoding(string es) { + try { + return Encoding.GetEncoding(es); + } + catch { + return Encoding.Default; + } + } + } + + public static Task ReadResponseStream( + this HttpResponseMessage httpResponse, + Func? writer, + CancellationToken cancellationToken = default + ) { + var readTask = writer == null ? ReadResponse() : ReadAndConvertResponse(writer); + return readTask; + + Task ReadResponse() { +#if NET + return httpResponse.Content.ReadAsStreamAsync(cancellationToken)!; +# else + return httpResponse.Content == null ? Task.FromResult((Stream?)null) : httpResponse.Content.ReadAsStreamAsync(); +#endif + } + + async Task ReadAndConvertResponse(Func streamWriter) { +#if NET + await using var original = await ReadResponse().ConfigureAwait(false); +#else + using var original = await ReadResponse().ConfigureAwait(false); +#endif + return original == null ? null : streamWriter(original); + } + } +} \ No newline at end of file diff --git a/src/RestSharp/Extensions/ReflectionExtensions.cs b/src/RestSharp/Extensions/ReflectionExtensions.cs new file mode 100644 index 000000000..01a7e2b96 --- /dev/null +++ b/src/RestSharp/Extensions/ReflectionExtensions.cs @@ -0,0 +1,90 @@ +// 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. + +using System.Globalization; +using System.Reflection; + +namespace RestSharp.Extensions; + +/// +/// Reflection extensions +/// +public static class ReflectionExtensions { + /// + /// Retrieve an attribute from a member (property) + /// + /// Type of attribute to retrieve + /// Member to retrieve attribute from + /// + public static T? GetAttribute(this MemberInfo prop) where T : Attribute => Attribute.GetCustomAttribute(prop, typeof(T)) as T; + + /// + /// Retrieve an attribute from a type + /// + /// Type of attribute to retrieve + /// Type to retrieve attribute from + /// + public static T? GetAttribute(this Type type) where T : Attribute => Attribute.GetCustomAttribute(type, typeof(T)) as T; + + /// + /// Checks a type to see if it derives from a raw generic (e.g. List[[]]) + /// + /// + /// + /// + public static bool IsSubclassOfRawGeneric(this Type? toCheck, Type generic) { + while (toCheck != null && toCheck != typeof(object)) { + var cur = toCheck.GetTypeInfo().IsGenericType + ? toCheck.GetGenericTypeDefinition() + : toCheck; + + if (generic == cur) return true; + + toCheck = toCheck.GetTypeInfo().BaseType; + } + + return false; + } + + internal static object? ChangeType(this object? source, Type newType) => Convert.ChangeType(source, newType); + + /// + /// Find a value from a System.Enum by trying several possible variants + /// of the string value of the enum. + /// + /// Type of enum + /// Value for which to search + /// The culture used to calculate the name variants + /// + public static object? FindEnumValue(this Type type, string value, CultureInfo culture) { + var caseInsensitiveComparer = StringComparer.Create(culture, true); + + var ret = Enum.GetValues(type) + .Cast() + .FirstOrDefault( + v => v.ToString() + .GetNameVariants(culture) + .Contains(value, caseInsensitiveComparer) + ); + + if (ret != null) return ret; + + var enumValueAsUnderlyingType = Convert.ChangeType(value, Enum.GetUnderlyingType(type), culture); + + if (Enum.IsDefined(type, enumValueAsUnderlyingType)) + ret = (Enum)Enum.ToObject(type, enumValueAsUnderlyingType); + + return ret ?? Activator.CreateInstance(type); + } +} \ No newline at end of file diff --git a/src/RestSharp/Extensions/StreamExtensions.cs b/src/RestSharp/Extensions/StreamExtensions.cs new file mode 100644 index 000000000..fd5cf685b --- /dev/null +++ b/src/RestSharp/Extensions/StreamExtensions.cs @@ -0,0 +1,42 @@ +// 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. + +namespace RestSharp.Extensions; + +/// +/// Extension method overload! +/// +static class StreamExtensions { + /// + /// Read a stream into a byte array + /// + /// Stream to read + /// + /// byte[] + public static async Task ReadAsBytes(this Stream input, CancellationToken cancellationToken) { + var buffer = new byte[16 * 1024]; + + using var ms = new MemoryStream(); + + int read; +#if NET + while ((read = await input.ReadAsync(buffer, cancellationToken).ConfigureAwait(false)) > 0) +#else + while ((read = await input.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) > 0) +#endif + ms.Write(buffer, 0, read); + + return ms.ToArray(); + } +} \ No newline at end of file diff --git a/src/RestSharp/Extensions/StringExtensions.cs b/src/RestSharp/Extensions/StringExtensions.cs new file mode 100644 index 000000000..637177a67 --- /dev/null +++ b/src/RestSharp/Extensions/StringExtensions.cs @@ -0,0 +1,255 @@ +// 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. + +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Text; +using System.Text.RegularExpressions; +using System.Web; + +namespace RestSharp.Extensions; + +// ReSharper disable once PartialTypeWithSinglePart +static partial class StringExtensions { + static readonly Regex IsUpperCaseRegex = IsUpperCase(); + + static readonly Regex AddUnderscoresRegex1 = AddUnderscores1(); + static readonly Regex AddUnderscoresRegex2 = AddUnderscores2(); + static readonly Regex AddUnderscoresRegex3 = AddUnderscores3(); + + static readonly Regex AddDashesRegex1 = AddDashes1(); + static readonly Regex AddDashesRegex2 = AddDashes2(); + static readonly Regex AddDashesRegex3 = AddDashes3(); + + static readonly Regex AddSpacesRegex1 = AddSpaces1(); + static readonly Regex AddSpacesRegex2 = AddSpaces2(); + static readonly Regex AddSpacesRegex3 = AddSpaces3(); + + extension(string input) { + internal string UrlDecode() => HttpUtility.UrlDecode(input); + + /// + /// Uses Uri.EscapeDataString() based on recommendations on MSDN + /// http://blogs.msdn.com/b/yangxind/archive/2006/11/09/don-t-use-net-system-uri-unescapedatastring-in-url-decoding.aspx + /// + internal string UrlEncode() { + const int maxLength = 32766; + + if (input == null) throw new ArgumentNullException(nameof(input)); + + if (input.Length <= maxLength) return Uri.EscapeDataString(input); + + var sb = new StringBuilder(input.Length * 2); + var index = 0; + + while (index < input.Length) { + var length = Math.Min(input.Length - index, maxLength); + + while (CharUnicodeInfo.GetUnicodeCategory(input[index + length - 1]) == UnicodeCategory.Surrogate) { + length--; + } + + var subString = input.Substring(index, length); + + sb.Append(Uri.EscapeDataString(subString)); + index += subString.Length; + } + + return sb.ToString(); + } + + internal string? UrlEncode(Encoding encoding) { + var encoded = HttpUtility.UrlEncode(input, encoding); + return encoded?.Replace("+", "%20"); + } + } + + extension(string input) { + internal string RemoveUnderscoresAndDashes() => input.Replace("_", "").Replace("-", ""); + + internal string ToPascalCase(CultureInfo culture) => ToPascalCase(input, true, culture); + + internal string ToPascalCase(bool removeUnderscores, CultureInfo culture) { + if (string.IsNullOrEmpty(input)) return input; + + input = input.Replace('_', ' '); + + var joinString = removeUnderscores ? string.Empty : "_"; + var words = input.Split(' '); + + return words + .Where(x => x.Length > 0) + .Select(CaseWord) + .JoinToString(joinString); + + string CaseWord(string word) { + var restOfWord = word[1..]; + var firstChar = char.ToUpper(word[0], culture); + + if (restOfWord.IsUpperCase()) restOfWord = restOfWord.ToLower(culture); + + return string.Concat(firstChar, restOfWord); + } + } + + internal string ToCamelCase(CultureInfo culture) + => MakeInitialLowerCase(ToPascalCase(input, culture), culture); + + internal IEnumerable GetNameVariants(CultureInfo culture) { + if (string.IsNullOrEmpty(input)) yield break; + + yield return input; + + // try camel cased name + yield return input.ToCamelCase(culture); + + // try lower cased name + yield return input.ToLower(culture); + + // try name with underscores + yield return input.AddUnderscores(); + + // try name with underscores with lower case + yield return input.AddUnderscores().ToLower(culture); + + // try name with dashes + yield return input.AddDashes(); + + // try name with dashes with lower case + yield return input.AddDashes().ToLower(culture); + + // try name with underscore prefix + yield return input.AddUnderscorePrefix(); + + // try name with proper camel case + yield return input.AddUnderscores().ToCamelCase(culture); + + // try name with underscore prefix, using proper camel case + yield return input.ToCamelCase(culture).AddUnderscorePrefix(); + + // try name with underscore prefix, using camel case + yield return input.AddUnderscores().ToCamelCase(culture).AddUnderscorePrefix(); + + // try name with spaces + yield return input.AddSpaces(); + + // try name with spaces with lower case + yield return input.AddSpaces().ToLower(culture); + } + } + + internal static bool IsEmpty([NotNullWhen(false)] this string? value) => string.IsNullOrWhiteSpace(value); + + internal static bool IsNotEmpty([NotNullWhen(true)] this string? value) => !string.IsNullOrWhiteSpace(value); + + internal static string JoinToString(this IEnumerable strings, string separator) => string.Join(separator, strings); + + extension(string word) { + string MakeInitialLowerCase(CultureInfo culture) => string.Concat(word[..1].ToLower(culture), word[1..]); + + string AddUnderscores() + => AddUnderscoresRegex1.Replace( + AddUnderscoresRegex2.Replace( + AddUnderscoresRegex3.Replace(word, "$1_$2"), + "$1_$2" + ), + "_" + ); + + string AddDashes() + => AddDashesRegex1.Replace( + AddDashesRegex2.Replace( + AddDashesRegex3.Replace(word, "$1-$2"), + "$1-$2" + ), + "-" + ); + + bool IsUpperCase() => IsUpperCaseRegex.IsMatch(word); + + string AddUnderscorePrefix() => $"_{word}"; + + string AddSpaces() + => AddSpacesRegex1.Replace( + AddSpacesRegex2.Replace( + AddSpacesRegex3.Replace(word, "$1 $2"), + "$1 $2" + ), + " " + ); + } + + const string RIsUpperCase = "^[A-Z]+$"; + const string RAddUnderscore1 = @"[-\s]"; + const string RAddUnderscore2 = @"([a-z\d])([A-Z])"; + const string RAddUnderscore3 = "([A-Z]+)([A-Z][a-z])"; + const string RAddDashes1 = @"[\s]"; + const string RAddDashes2 = @"([a-z\d])([A-Z])"; + const string RAddDashes3 = "([A-Z]+)([A-Z][a-z])"; + const string RAddSpaces1 = @"[-\s]"; + const string RAddSpaces2 = @"([a-z\d])([A-Z])"; + const string RAddSpaces3 = "([A-Z]+)([A-Z][a-z])"; + +#if NET7_0_OR_GREATER + [GeneratedRegex(RIsUpperCase)] + private static partial Regex IsUpperCase(); + + [GeneratedRegex(RAddUnderscore1)] + private static partial Regex AddUnderscores1(); + + [GeneratedRegex(RAddUnderscore2)] + private static partial Regex AddUnderscores2(); + + [GeneratedRegex(RAddUnderscore3)] + private static partial Regex AddUnderscores3(); + + [GeneratedRegex(RAddDashes1)] + private static partial Regex AddDashes1(); + + [GeneratedRegex(RAddDashes2)] + private static partial Regex AddDashes2(); + + [GeneratedRegex(RAddDashes3)] + private static partial Regex AddDashes3(); + + [GeneratedRegex(RAddSpaces1)] + private static partial Regex AddSpaces1(); + + [GeneratedRegex(RAddSpaces2)] + private static partial Regex AddSpaces2(); + + [GeneratedRegex(RAddSpaces3)] + private static partial Regex AddSpaces3(); +#else + static Regex IsUpperCase() => new(RIsUpperCase); + + static Regex AddUnderscores1() => new(RAddUnderscore1); + + static Regex AddUnderscores2() => new(RAddUnderscore2); + + static Regex AddUnderscores3() => new(RAddUnderscore3); + + static Regex AddDashes1() => new(RAddDashes1); + + static Regex AddDashes2() => new(RAddDashes2); + + static Regex AddDashes3() => new(RAddDashes3); + + static Regex AddSpaces1() => new(RAddSpaces1); + + static Regex AddSpaces2() => new(RAddSpaces1); + + static Regex AddSpaces3() => new(RAddSpaces1); +#endif +} \ No newline at end of file diff --git a/src/RestSharp/Extensions/WithExtensions.cs b/src/RestSharp/Extensions/WithExtensions.cs new file mode 100644 index 000000000..50d15c889 --- /dev/null +++ b/src/RestSharp/Extensions/WithExtensions.cs @@ -0,0 +1,22 @@ +// 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. + +namespace RestSharp.Extensions; + +static class WithExtensions { + internal static T With(this T self, Action @do) { + @do(self); + return self; + } +} \ No newline at end of file diff --git a/src/RestSharp/IRestClient.cs b/src/RestSharp/IRestClient.cs new file mode 100644 index 000000000..3f37b536d --- /dev/null +++ b/src/RestSharp/IRestClient.cs @@ -0,0 +1,50 @@ +// 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. +// + +using RestSharp.Serializers; + +namespace RestSharp; + +public interface IRestClient : IDisposable { + /// + /// Client options that aren't used for configuring HttpClient + /// + ReadOnlyRestClientOptions Options { get; } + + /// + /// Client-level serializers + /// + RestSerializers Serializers { get; } + + /// + /// Default parameters to use on every request made with this client instance. + /// + DefaultParameters DefaultParameters { get; } + + /// + /// Executes the request asynchronously, authenticating if needed + /// + /// Request to be executed + /// Cancellation token + Task ExecuteAsync(RestRequest request, CancellationToken cancellationToken = default); + + /// + /// A specialized method to download files as streams. + /// + /// Pre-configured request instance. + /// + /// The downloaded stream. + Task DownloadStreamAsync(RestRequest request, CancellationToken cancellationToken = default); +} diff --git a/src/RestSharp/Interceptors/CompatibilityInterceptor.cs b/src/RestSharp/Interceptors/CompatibilityInterceptor.cs new file mode 100644 index 000000000..34ac4354c --- /dev/null +++ b/src/RestSharp/Interceptors/CompatibilityInterceptor.cs @@ -0,0 +1,43 @@ +// 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. + +// ReSharper disable UnusedAutoPropertyAccessor.Global +namespace RestSharp.Interceptors; + +/// +/// This class allows easier migration of legacy request hooks to interceptors. +/// +public class CompatibilityInterceptor : Interceptor { + public Action? OnBeforeDeserialization { get; set; } + public Func? OnBeforeRequest { get; set; } + public Func? OnAfterRequest { get; set; } + + /// + public override ValueTask BeforeDeserialization(RestResponse response, CancellationToken cancellationToken) { + OnBeforeDeserialization?.Invoke(response); + return default; + } + + public override async ValueTask BeforeHttpRequest(HttpRequestMessage requestMessage, CancellationToken cancellationToken) { + if (OnBeforeRequest != null) { + await OnBeforeRequest(requestMessage); + } + } + + public override async ValueTask AfterHttpRequest(HttpResponseMessage responseMessage, CancellationToken cancellationToken) { + if (OnAfterRequest != null) { + await OnAfterRequest(responseMessage); + } + } +} \ No newline at end of file diff --git a/src/RestSharp/Interceptors/Interceptor.cs b/src/RestSharp/Interceptors/Interceptor.cs new file mode 100644 index 000000000..ddd765f52 --- /dev/null +++ b/src/RestSharp/Interceptors/Interceptor.cs @@ -0,0 +1,62 @@ +// 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. +// + +namespace RestSharp.Interceptors; + +/// +/// Base Interceptor +/// +public abstract class Interceptor { + static readonly ValueTask Completed = +#if NET + ValueTask.CompletedTask; +#else + new (); +#endif + /// + /// Intercepts the request before composing the request message + /// + /// RestRequest before composing the request message + /// Cancellation token + public virtual ValueTask BeforeRequest(RestRequest request, CancellationToken cancellationToken) => Completed; + + /// + /// Intercepts the request before being sent + /// + /// HttpRequestMessage before being sent + /// Cancellation token + public virtual ValueTask BeforeHttpRequest(HttpRequestMessage requestMessage, CancellationToken cancellationToken) => Completed; + + /// + /// Intercepts the request before being sent + /// + /// HttpResponseMessage as received from the remote server + /// Cancellation token + public virtual ValueTask AfterHttpRequest(HttpResponseMessage responseMessage, CancellationToken cancellationToken) => Completed; + + /// + /// Intercepts the request after it's created from HttpResponseMessage + /// + /// HttpResponseMessage as received from the remote server + /// Cancellation token + public virtual ValueTask AfterRequest(RestResponse response, CancellationToken cancellationToken) => Completed; + + /// + /// Intercepts the request before deserialization, won't be called if using non-generic ExecuteAsync + /// + /// HttpResponseMessage as received from the remote server + /// Cancellation token + public virtual ValueTask BeforeDeserialization(RestResponse response, CancellationToken cancellationToken) => Completed; +} diff --git a/src/RestSharp/KnownHeaders.cs b/src/RestSharp/KnownHeaders.cs new file mode 100644 index 000000000..edf9d47bb --- /dev/null +++ b/src/RestSharp/KnownHeaders.cs @@ -0,0 +1,50 @@ +// 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. +// + +// ReSharper disable InconsistentNaming +// ReSharper disable MemberCanBePrivate.Global + +namespace RestSharp; + +public static class KnownHeaders { + public const string Authorization = "Authorization"; + public const string Accept = "Accept"; + public const string AcceptLanguage = "Accept-Language"; + public const string Allow = "Allow"; + public const string Expires = "Expires"; + public const string ContentDisposition = "Content-Disposition"; + public const string ContentEncoding = "Content-Encoding"; + public const string ContentLanguage = "Content-Language"; + public const string ContentLength = "Content-Length"; + public const string ContentLocation = "Content-Location"; + public const string ContentRange = "Content-Range"; + public const string ContentType = "Content-Type"; + public const string KeepAlive = "Keep-Alive"; + public const string LastModified = "Last-Modified"; + public const string ContentMD5 = "Content-MD5"; + public const string Host = "Host"; + public const string Cookie = "Cookie"; + public const string SetCookie = "Set-Cookie"; + public const string UserAgent = "User-Agent"; + + internal static readonly string[] ContentHeaders = [ + Allow, Expires, ContentDisposition, ContentEncoding, ContentLanguage, ContentLength, ContentLocation, ContentRange, ContentType, ContentMD5, + LastModified + ]; + + static readonly HashSet ContentHeadersHash = new(ContentHeaders, StringComparer.InvariantCultureIgnoreCase); + + internal static bool IsContentHeader(string key) => ContentHeadersHash.Contains(key); +} \ No newline at end of file diff --git a/src/RestSharp/Options/ReadOnlyRestClientOptions.cs b/src/RestSharp/Options/ReadOnlyRestClientOptions.cs new file mode 100644 index 000000000..9ba2331d2 --- /dev/null +++ b/src/RestSharp/Options/ReadOnlyRestClientOptions.cs @@ -0,0 +1,32 @@ +// 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. +// + +using System.Collections.ObjectModel; +using RestSharp.Interceptors; + +namespace RestSharp; + +public partial class ReadOnlyRestClientOptions { + public IReadOnlyCollection? Interceptors { get; private set; } + + partial void CopyAdditionalProperties(RestClientOptions inner) => Interceptors = GetInterceptors(inner); + + static ReadOnlyCollection? GetInterceptors(RestClientOptions? options) { + if (options == null || options.Interceptors.Count == 0) return null; + + var interceptors = new List(options.Interceptors); + return interceptors.AsReadOnly(); + } +} diff --git a/src/RestSharp/Options/RestClientOptions.cs b/src/RestSharp/Options/RestClientOptions.cs new file mode 100644 index 000000000..d30e34f92 --- /dev/null +++ b/src/RestSharp/Options/RestClientOptions.cs @@ -0,0 +1,233 @@ +// 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. +// + +using System.Net.Http.Headers; +using System.Net.Security; +using System.Reflection; +#if NET +using System.Runtime.Versioning; +#endif +using System.Security.Cryptography.X509Certificates; +using System.Text; +using RestSharp.Authenticators; +using RestSharp.Extensions; +using RestSharp.Interceptors; + +// ReSharper disable UnusedAutoPropertyAccessor.Global +// ReSharper disable PropertyCanBeMadeInitOnly.Global +// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global + +namespace RestSharp; + +[GenerateImmutable] +[GenerateClone(BaseType = typeof(RestClientOptions), Name = "CopyFrom", Mutate = true)] +public partial class RestClientOptions { + static readonly Version Version = new AssemblyName(typeof(RestClientOptions).Assembly.FullName!).Version!; + + static readonly string DefaultUserAgent = $"RestSharp/{Version}"; + + public RestClientOptions() { } + + // ReSharper disable once MemberCanBePrivate.Global + public RestClientOptions(Uri baseUrl) => BaseUrl = baseUrl; + + public RestClientOptions(string baseUrl) : this(new Uri(Ensure.NotEmptyString(baseUrl, nameof(baseUrl)))) { } + + /// + /// Base URL for all requests made with this client instance + /// + public Uri? BaseUrl { get; set; } + + /// + /// Custom configuration for the underlying + /// + public Func? ConfigureMessageHandler { get; set; } + + /// + /// Function to calculate the response status. By default, the status will be Completed if it was successful, or NotFound. + /// + public CalculateResponseStatus CalculateResponseStatus { get; set; } = httpResponse + => httpResponse.IsSuccessStatusCode || httpResponse.StatusCode == HttpStatusCode.NotFound + ? ResponseStatus.Completed + : ResponseStatus.Error; + + /// + /// Authenticator that will be used to populate request with necessary authentication data + /// + public IAuthenticator? Authenticator { get; set; } + + /// + /// List of interceptors that will be executed before the request is sent + /// + [Exclude] + public List Interceptors { get; set; } = []; + + /// + /// Passed to Credentials property + /// +#if NET + [UnsupportedOSPlatform("browser")] +#endif + public ICredentials? Credentials { get; set; } + + /// + /// Determine whether the "default credentials" (e.g. the user account under which the current process is + /// running) will be sent along to the server. The default is false. + /// Passed to UseDefaultCredentials property + /// +#if NET + [UnsupportedOSPlatform("browser")] +#endif + public bool UseDefaultCredentials { get; set; } + + /// + /// Set to true if you need the Content-Type not to have the charset + /// + public bool DisableCharset { get; set; } + + /// + /// Set the decompression method to use when making requests + /// +#if NET + [UnsupportedOSPlatform("browser")] + public DecompressionMethods AutomaticDecompression { get; set; } = DecompressionMethods.All; +#else + public DecompressionMethods AutomaticDecompression { get; set; } = DecompressionMethods.GZip; +#endif + + /// + /// Set the maximum number of redirects to follow + /// +#if NET + [UnsupportedOSPlatform("browser")] +#endif + public int? MaxRedirects { get; set; } + + /// + /// X509CertificateCollection to be sent with request + /// +#if NET + [UnsupportedOSPlatform("browser")] +#endif + [Exclude] + public X509CertificateCollection? ClientCertificates { get; set; } + + /// + /// Set the proxy to use when making requests. Default is null, which will use the default system proxy if one is set. + /// +#if NET + [UnsupportedOSPlatform("browser")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] +#endif + public IWebProxy? Proxy { get; set; } + + /// + /// Cache policy to be used for requests using + /// + public CacheControlHeaderValue? CachePolicy { get; set; } + + /// + /// Instruct the client to follow redirects. Default is true. + /// + public bool FollowRedirects { get; set; } = true; + + /// + /// Gets or sets a value that indicates if the header for an HTTP request contains Continue. + /// + public bool? Expect100Continue { get; set; } = null; + + /// + /// Value of the User-Agent header to be sent with requests. Default is "RestSharp/{version}" + /// + public string? UserAgent { get; set; } = DefaultUserAgent; + + /// + /// Passed to property + /// +#if NET + [UnsupportedOSPlatform("browser")] +#endif + public bool PreAuthenticate { get; set; } + + /// + /// Callback function for handling the validation of remote certificates. Useful for certificate pinning and + /// overriding certificate errors in the scope of a request. + /// +#if NET + [UnsupportedOSPlatform("browser")] +#endif + public RemoteCertificateValidationCallback? RemoteCertificateValidationCallback { get; set; } + + /// + /// Sets the value of the Host header to be sent with requests. + /// + public string? BaseHost { get; set; } + + /// + /// Custom cookie container to be used for requests. RestSharp will not assign the container to the message handler, + /// but will fetch cookies from it and set them on the request. + /// + public CookieContainer? CookieContainer { get; set; } + + /// + /// Request duration. Used when the request timeout is not specified using , + /// + public TimeSpan? Timeout { get; set; } + + /// + /// Default encoding to use when no encoding is specified in the content type header. + /// + public Encoding Encoding { get; set; } = Encoding.UTF8; + + /// + /// Set to true to throw an exception when a deserialization error occurs. Default is false. + /// + public bool ThrowOnDeserializationError { get; set; } + + /// + /// When set to true, the response status will be set to + /// when a deserialization error occurs. Default is true. + /// + public bool FailOnDeserializationError { get; set; } = true; + + /// + /// Set to true to throw an exception when throws an exception when making a request. + /// Default is false. + /// + public bool ThrowOnAnyError { get; set; } + + /// + /// When set to false, the client doesn't set the `ErrorException` property for responses with unsuccessful status codes. + /// Default is true. + /// + public bool SetErrorExceptionOnUnsuccessfulStatusCode { 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. + /// + public bool AllowMultipleDefaultParametersWithSameName { get; set; } + + /// + /// Custom function to encode a string for use in a URL. + /// + public Func Encode { get; set; } = s => s.UrlEncode(); + + /// + /// Custom function to encode a string for use in a URL query. + /// + public Func EncodeQuery { get; set; } = (s, encoding) => s.UrlEncode(encoding)!; +} diff --git a/src/RestSharp/Parameters/BodyParameter.cs b/src/RestSharp/Parameters/BodyParameter.cs new file mode 100644 index 000000000..09948880d --- /dev/null +++ b/src/RestSharp/Parameters/BodyParameter.cs @@ -0,0 +1,62 @@ +// 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. +// + +namespace RestSharp; + +public record BodyParameter : Parameter { + public BodyParameter(string? name, object value, ContentType contentType, DataFormat dataFormat = DataFormat.None) + : base(name, Ensure.NotNull(value, nameof(value)), ParameterType.RequestBody, false) { + if (dataFormat == DataFormat.Binary && value is not byte[]) { + throw new ArgumentException("Binary data format needs a byte array as value"); + } + + ContentType = contentType; + DataFormat = dataFormat; + } + + public BodyParameter(object value, ContentType contentType, DataFormat dataFormat = DataFormat.None) + : this("", value, contentType, dataFormat) { } + + /// + /// Body parameter data type + /// + public DataFormat DataFormat { get; init; } = DataFormat.None; + + /// + /// Custom content encoding + /// + public string? ContentEncoding { get; init; } +} + +public record XmlParameter : BodyParameter { + [PublicAPI] + public XmlParameter(string name, object value, string? xmlNamespace = null, ContentType? contentType = null) + : base(name, value, contentType ?? ContentType.Xml, DataFormat.Xml) + => XmlNamespace = xmlNamespace; + + public XmlParameter(object value, string? xmlNamespace = null, ContentType? contentType = null) + : this("", value, xmlNamespace, contentType) { } + + public string? XmlNamespace { get; } +} + +public record JsonParameter : BodyParameter { + [PublicAPI] + public JsonParameter(string name, object value, ContentType? contentType = null) + : base(name, value, contentType ?? ContentType.Json, DataFormat.Json) { } + + public JsonParameter(object value, ContentType? contentType = null) + : this("", value, contentType) { } +} diff --git a/src/RestSharp/Parameters/DefaultParameters.cs b/src/RestSharp/Parameters/DefaultParameters.cs new file mode 100644 index 000000000..a65b41846 --- /dev/null +++ b/src/RestSharp/Parameters/DefaultParameters.cs @@ -0,0 +1,72 @@ +// 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. +// + +using System.Runtime.CompilerServices; + +namespace RestSharp; + +public sealed class DefaultParameters(ReadOnlyRestClientOptions options) : ParametersCollection { + /// + /// Safely add a default parameter to the collection. + /// + /// Parameter to add + /// + /// + /// + [MethodImpl(MethodImplOptions.Synchronized)] + public DefaultParameters AddParameter(Parameter parameter) { + if (parameter.Type == ParameterType.RequestBody) + throw new NotSupportedException("Cannot set request body using default parameters. Use Request.AddBody() instead."); + + if (!options.AllowMultipleDefaultParametersWithSameName && + parameter.Type != ParameterType.HttpHeader && + !MultiParameterTypes.Contains(parameter.Type) && + this.Any(x => x.Name == parameter.Name)) { + throw new ArgumentException("A default parameters with the same name has already been added", nameof(parameter)); + } + + Parameters.Add(parameter); + + return this; + } + + /// + /// Safely removes all the default parameters with the given name and type. + /// + /// Parameter name + /// Parameter type + /// + [PublicAPI] + [MethodImpl(MethodImplOptions.Synchronized)] + public DefaultParameters RemoveParameter(string name, ParameterType type) { + Parameters.RemoveAll(x => x.Name == name && x.Type == type); + + return this; + } + + /// + /// Replace a default parameter with the same name and type. + /// + /// Parameter instance + /// + [PublicAPI] + public DefaultParameters ReplaceParameter(Parameter parameter) + => + // ReSharper disable once NotResolvedInText + RemoveParameter(Ensure.NotEmptyString(parameter.Name, "Parameter name"), parameter.Type) + .AddParameter(parameter); + + static readonly ParameterType[] MultiParameterTypes = [ParameterType.QueryString, ParameterType.GetOrPost]; +} \ No newline at end of file diff --git a/src/RestSharp/Parameters/FileParameter.cs b/src/RestSharp/Parameters/FileParameter.cs new file mode 100644 index 000000000..d490d9fd3 --- /dev/null +++ b/src/RestSharp/Parameters/FileParameter.cs @@ -0,0 +1,118 @@ +// 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. + +namespace RestSharp; + +/// +/// Container for files to be uploaded with requests +/// +[PublicAPI] +public record FileParameter { + /// + /// Name of the parameter + /// + public string Name { get; } + + /// + /// Name of the file to use when uploading + /// + public string FileName { get; } + + /// + /// MIME content type of file + /// + public ContentType ContentType { get; } + + /// + /// Provides raw data for file + /// + public Func GetFile { get; } + + public FileParameterOptions Options { get; } + + FileParameter(string name, string fileName, Func getFile, ContentType? contentType, FileParameterOptions options) { + Name = name; + FileName = fileName; + GetFile = getFile; + Options = options; + ContentType = contentType ?? ContentType.Binary; + } + + /// + /// Creates a file parameter from an array of bytes. + /// + /// The parameter name to use in the request. + /// The data to use as the file's contents. + /// The filename to use in the request. + /// The content type to use in the request. + /// File parameter options + /// The + public static FileParameter Create( + string name, + byte[] data, + string filename, + ContentType? contentType = null, + FileParameterOptions? options = null + ) { + return new(name, filename, GetFile, contentType, options ?? new FileParameterOptions()); + + Stream GetFile() { + var stream = new MemoryStream(); + stream.Write(data, 0, data.Length); + stream.Flush(); + stream.Seek(0, SeekOrigin.Begin); + return stream; + } + } + + /// + /// Creates a file parameter from an array of bytes. + /// + /// The parameter name to use in the request. + /// Delegate that will be called with the request stream so you can write to it.. + /// The filename to use in the request. + /// Optional: parameter content type, default is "application/g-zip" + /// File parameter options + /// The using the default content type. + public static FileParameter Create( + string name, + Func getFile, + string fileName, + ContentType? contentType = null, + FileParameterOptions? options = null + ) + => new(name, fileName, getFile, contentType, options ?? new FileParameterOptions()); + + public static FileParameter FromFile( + string fullPath, + string? name = null, + ContentType? contentType = null, + FileParameterOptions? options = null + ) { + if (!File.Exists(Ensure.NotEmptyString(fullPath, nameof(fullPath)))) throw new FileNotFoundException("File not found", fullPath); + + var fileName = Path.GetFileName(fullPath); + var parameterName = name ?? fileName; + + return new(parameterName, fileName, GetFile, contentType, options ?? new FileParameterOptions()); + + Stream GetFile() => File.OpenRead(fullPath); + } +} + +[PublicAPI] +public class FileParameterOptions { + public bool DisableFilenameStar { get; set; } = true; + public bool DisableFilenameEncoding { get; set; } +} diff --git a/src/RestSharp/Parameters/GetOrPostParameter.cs b/src/RestSharp/Parameters/GetOrPostParameter.cs new file mode 100644 index 000000000..b9309475e --- /dev/null +++ b/src/RestSharp/Parameters/GetOrPostParameter.cs @@ -0,0 +1,25 @@ +// 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. + +namespace RestSharp; + +public record GetOrPostParameter : NamedParameter { + /// + /// Instantiates an HTTP parameter instance (QueryString for GET, DELETE, OPTIONS and HEAD; Encoded form for POST and PUT) + /// + /// Name of the parameter + /// Value of the parameter + /// Encode the value or not, default true + public GetOrPostParameter(string name, string? value, bool encode = true) : base(name, value, ParameterType.GetOrPost, encode) { } +} \ No newline at end of file diff --git a/src/RestSharp/Parameters/HeaderParameter.cs b/src/RestSharp/Parameters/HeaderParameter.cs new file mode 100644 index 000000000..76fc0b75a --- /dev/null +++ b/src/RestSharp/Parameters/HeaderParameter.cs @@ -0,0 +1,82 @@ +// 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. +// + +using System.Text; +using System.Text.RegularExpressions; + +namespace RestSharp; + +public partial record HeaderParameter : Parameter { + /// + /// Instantiates a header parameter + /// + /// Header name + /// Header value + /// Set to true to encode header value according to RFC 2047. Default is false. + public HeaderParameter(string name, string value, bool encode = false) + : base( + EnsureValidHeaderString(Ensure.NotEmptyString(name, nameof(name)), "name"), + EnsureValidHeaderValue(name, value, encode), + ParameterType.HttpHeader, + false + ) { } + + public new string Name => base.Name!; + public new string Value => (string)base.Value!; + + static string EnsureValidHeaderValue(string name, string value, bool encode) { + CheckAndThrowsForInvalidHost(name, value); + + return EnsureValidHeaderString(GetValue(Ensure.NotNull(value, nameof(value)), encode), "value"); + } + + static string EnsureValidHeaderString(string value, string type) + => !IsInvalidHeaderString(value) ? value : throw new ArgumentException($"Invalid character found in header {type}: {value}"); + + static string GetValue(string value, bool encode) => encode ? GetBase64EncodedHeaderValue(value) : value; + + static string GetBase64EncodedHeaderValue(string value) => $"=?UTF-8?B?{Convert.ToBase64String(Encoding.UTF8.GetBytes(value))}?="; + + static bool IsInvalidHeaderString(string stringValue) { + // ReSharper disable once ForCanBeConvertedToForeach + for (var i = 0; i < stringValue.Length; i++) { + switch (stringValue[i]) { + case '\r': + case '\n': + return true; + } + } + + return false; + } + + static readonly Regex PortSplitRegex = PartSplit(); + + static void CheckAndThrowsForInvalidHost(string name, string value) { + if (name == KnownHeaders.Host && InvalidHost(value)) + throw new ArgumentException("The specified value is not a valid Host header string.", nameof(value)); + + return; + + static bool InvalidHost(string host) => Uri.CheckHostName(PortSplitRegex.Split(host)[0]) == UriHostNameType.Unknown; + } + +#if NET7_0_OR_GREATER + [GeneratedRegex(@":\d+")] + private static partial Regex PartSplit(); +#else + static Regex PartSplit() => new(@":\d+"); +#endif +} \ No newline at end of file diff --git a/src/RestSharp/Parameters/ObjectParser.cs b/src/RestSharp/Parameters/ObjectParser.cs new file mode 100644 index 000000000..3f3464d97 --- /dev/null +++ b/src/RestSharp/Parameters/ObjectParser.cs @@ -0,0 +1,89 @@ +// 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. +// + +using System.Reflection; + +namespace RestSharp; + +static class ObjectParser { + public static IEnumerable GetProperties(this object obj, params string[] includedProperties) { + // automatically create parameters from object props + var type = obj.GetType(); + var props = type.GetProperties(); + + var properties = new List(); + + foreach (var prop in props.Where(x => IsAllowedProperty(x.Name))) { + var val = prop.GetValue(obj, null); + + if (val == null) continue; + + if (prop.PropertyType.IsArray) + properties.AddRange(GetArray(prop, val)); + else + properties.Add(GetValue(prop, val)); + } + + return properties; + + ParsedParameter GetValue(PropertyInfo propertyInfo, object? value) { + var attribute = propertyInfo.GetCustomAttribute(); + var name = attribute?.Name ?? propertyInfo.Name; + var val = ParseValue(attribute?.Format, value); + return new(name, val, attribute?.Encode ?? true); + } + + IEnumerable GetArray(PropertyInfo propertyInfo, object? value) { + var elementType = propertyInfo.PropertyType.GetElementType(); + var array = (Array)value!; + + var attribute = propertyInfo.GetCustomAttribute(); + var name = attribute?.Name ?? propertyInfo.Name; + var queryType = attribute?.ArrayQueryType ?? RequestArrayQueryType.CommaSeparated; + var encode = attribute?.Encode ?? true; + + if (array.Length <= 0 || elementType == null) return [new(name, null, encode)]; + + // convert the array to an array of strings + var values = array + .Cast() + .Select(item => ParseValue(attribute?.Format, item)); + + return queryType switch { + RequestArrayQueryType.CommaSeparated => [new(name, string.Join(",", values), encode)], + RequestArrayQueryType.ArrayParameters => values.Select(x => new ParsedParameter($"{name}[]", x, encode)), + _ => throw new ArgumentOutOfRangeException() + }; + + } + + bool IsAllowedProperty(string propertyName) + => includedProperties.Length == 0 || includedProperties.Length > 0 && includedProperties.Contains(propertyName); + + string? ParseValue(string? format, object? value) => format == null ? value?.ToString() : string.Format($"{{0:{format}}}", value); + } +} + +record ParsedParameter(string Name, string? Value, bool Encode); + +[AttributeUsage(AttributeTargets.Property)] +public class RequestPropertyAttribute : Attribute { + public string? Name { get; set; } + public string? Format { get; set; } + public RequestArrayQueryType ArrayQueryType { get; set; } = RequestArrayQueryType.CommaSeparated; + public bool Encode { get; set; } = true; +} + +public enum RequestArrayQueryType { CommaSeparated, ArrayParameters } diff --git a/src/RestSharp/Parameters/Parameter.cs b/src/RestSharp/Parameters/Parameter.cs new file mode 100644 index 000000000..b23c592dc --- /dev/null +++ b/src/RestSharp/Parameters/Parameter.cs @@ -0,0 +1,105 @@ +// 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. + +using System.Diagnostics; + +namespace RestSharp; + +/// +/// Parameter container for REST requests +/// +[DebuggerDisplay($"{{{nameof(DebuggerDisplay)}()}}")] +public abstract record Parameter { + /// + /// Parameter container for REST requests + /// + protected Parameter(string? name, object? value, ParameterType type, bool encode) { + Name = name; + Value = value; + Type = type; + Encode = encode; + } + + /// + /// Content type of the parameter. Normally applies to the body parameter, or POST parameter in multipart requests. + /// + public ContentType ContentType { get; set; } = ContentType.Undefined; + + /// + /// Parameter name + /// + public string? Name { get; } + + /// + /// Parameter value + /// + public object? Value { get; } + + /// + /// Parameter type + /// + public ParameterType Type { get; } + + /// + /// Indicates if the parameter value should be encoded or not. + /// + public bool Encode { get; } + + /// + /// Return a human-readable representation of this parameter + /// + /// String + public sealed override string ToString() => Value == null ? $"{Name}" : $"{Name}={ValueString}"; + + protected virtual string ValueString => Value?.ToString() ?? "null"; + + /// + /// Creates a parameter object of based on the type + /// + /// Parameter name + /// Parameter value + /// Parameter type + /// Indicates if the parameter value should be encoded + /// + /// + public static Parameter CreateParameter(string? name, object? value, ParameterType type, bool encode = true) + // ReSharper disable once SwitchExpressionHandlesSomeKnownEnumValuesWithExceptionInDefault + => type switch { + ParameterType.GetOrPost => new GetOrPostParameter(Ensure.NotEmptyString(name, nameof(name)), value?.ToString(), encode), + ParameterType.UrlSegment => new UrlSegmentParameter(Ensure.NotEmptyString(name, nameof(name)), value?.ToString()!, encode), + ParameterType.HttpHeader => new HeaderParameter(name!, value?.ToString()!), + ParameterType.QueryString => new QueryParameter(Ensure.NotEmptyString(name, nameof(name)), value?.ToString(), encode), + _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) + }; + + [PublicAPI] + public void Deconstruct(out string? name, out object? value, out ParameterType type, out bool encode) { + name = Name; + value = Value; + type = Type; + encode = Encode; + } + + /// + /// Assists with debugging by displaying in the debugger output + /// + /// + [UsedImplicitly] + protected string DebuggerDisplay() => $"{GetType().Name.Replace("Parameter", "")} {ToString()}"; +} + +public record NamedParameter : Parameter { + protected NamedParameter(string name, object? value, ParameterType type, bool encode = true) + : base(Ensure.NotEmptyString(name, nameof(name)), value, type, encode) { } +} \ No newline at end of file diff --git a/src/RestSharp/Parameters/ParametersCollection.cs b/src/RestSharp/Parameters/ParametersCollection.cs new file mode 100644 index 000000000..299f2d498 --- /dev/null +++ b/src/RestSharp/Parameters/ParametersCollection.cs @@ -0,0 +1,43 @@ +// 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. +// + +using System.Collections; + +namespace RestSharp; + +public abstract class ParametersCollection : IReadOnlyCollection where T : Parameter { + protected readonly List Parameters = []; + + // public ParametersCollection(IEnumerable parameters) => _parameters.AddRange(parameters); + + static readonly Func SearchPredicate = (p, name) + => p.Name != null && p.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase); + + public bool Exists(T parameter) => Parameters.Any(p => SearchPredicate(p, parameter.Name) && p.Type == parameter.Type); + + public T? TryFind(string parameterName) => Parameters.FirstOrDefault(x => SearchPredicate(x, parameterName)); + + public IEnumerable GetParameters() where TParameter : class, T => Parameters.OfType(); + + public IEnumerator GetEnumerator() => Parameters.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public int Count => Parameters.Count; +} + +public abstract class ParametersCollection : ParametersCollection { + public IEnumerable GetParameters(ParameterType parameterType) => Parameters.Where(x => x.Type == parameterType); +} \ No newline at end of file diff --git a/src/RestSharp/Parameters/ParametersCollectionExtensions.cs b/src/RestSharp/Parameters/ParametersCollectionExtensions.cs new file mode 100644 index 000000000..7be33e8b9 --- /dev/null +++ b/src/RestSharp/Parameters/ParametersCollectionExtensions.cs @@ -0,0 +1,34 @@ +// 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. +// + +namespace RestSharp; + +static class ParametersCollectionExtensions { + extension(ParametersCollection parameters) { + internal IEnumerable GetQueryParameters(Method method) { + Func condition = + !IsPost(method) + ? p => p.Type is ParameterType.GetOrPost or ParameterType.QueryString + : p => p.Type is ParameterType.QueryString; + + return parameters.Where(p => condition(p)); + } + + internal IEnumerable GetContentParameters(Method method) + => IsPost(method) ? parameters.GetParameters() : []; + } + + static bool IsPost(Method method) => method is Method.Post or Method.Put or Method.Patch; +} diff --git a/src/RestSharp/Parameters/QueryParameter.cs b/src/RestSharp/Parameters/QueryParameter.cs new file mode 100644 index 000000000..57623d76c --- /dev/null +++ b/src/RestSharp/Parameters/QueryParameter.cs @@ -0,0 +1,26 @@ +// 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. +// + +namespace RestSharp; + +public record QueryParameter : NamedParameter { + /// + /// Instantiates a new query parameter instance that will be added to the request URL as {name}={value} part of the query string. + /// + /// Parameter name + /// Parameter value + /// Optional: encode the value, default is true + public QueryParameter(string name, string? value, bool encode = true) : base(name, value, ParameterType.QueryString, encode) { } +} \ No newline at end of file diff --git a/src/RestSharp/Parameters/RequestParameters.cs b/src/RestSharp/Parameters/RequestParameters.cs new file mode 100644 index 000000000..2d673f9bd --- /dev/null +++ b/src/RestSharp/Parameters/RequestParameters.cs @@ -0,0 +1,73 @@ +// 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. +// + +namespace RestSharp; + +/// +/// Collection of request parameters +/// +public sealed class RequestParameters : ParametersCollection { + /// + /// Create an empty parameters collection + /// + public RequestParameters() { } + + /// + /// Creates a parameters collection from a collection of parameter objects + /// + /// Collection of existing parameters + public RequestParameters(IEnumerable parameters) => Parameters.AddRange(parameters); + + /// + /// Adds multiple parameters to the collection + /// + /// Parameters to add + /// + // ReSharper disable once UnusedMethodReturnValue.Global + public ParametersCollection AddParameters(IEnumerable parameters) { + Parameters.AddRange(parameters); + return this; + } + + /// + /// Add parameters from another parameter collection + /// + /// + /// + // ReSharper disable once UnusedMember.Global + public ParametersCollection AddParameters(ParametersCollection parameters) { + Parameters.AddRange(parameters); + return this; + } + + /// + /// Adds a single parameter to the collection + /// + /// Parameter to add + public void AddParameter(Parameter parameter) => Parameters.Add(parameter); + + /// + /// Remove one or more parameters from the collection by name + /// + /// Name of the parameter to remove + // ReSharper disable once UnusedMember.Global + public void RemoveParameter(string name) => Parameters.RemoveAll(x => x.Name == name); + + /// + /// Remove parameter from the collection by reference + /// + /// Parameter to remove + public void RemoveParameter(Parameter parameter) => Parameters.Remove(parameter); +} diff --git a/src/RestSharp/Parameters/UrlSegmentParameter.cs b/src/RestSharp/Parameters/UrlSegmentParameter.cs new file mode 100644 index 000000000..66a5f25b0 --- /dev/null +++ b/src/RestSharp/Parameters/UrlSegmentParameter.cs @@ -0,0 +1,45 @@ +// 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. + +using System.Text.RegularExpressions; +using RestSharp.Extensions; + +namespace RestSharp; + +public partial record UrlSegmentParameter : NamedParameter { + static readonly Regex RegexPattern = Pattern(); + + /// + /// Instantiates a new query parameter instance that will be added to the request URL by replacing part of the absolute path. + /// The request resource should have a placeholder {name} that will be replaced with the parameter value when the request is made. + /// + /// Parameter name + /// Parameter value + /// Optional: encode the value, default is true + /// Optional: whether to replace all %2f and %2F in the parameter value with '/', default is true + public UrlSegmentParameter(string name, string? value, bool encode = true, bool replaceEncodedSlash = true) + : base( + name, + value.IsEmpty() ? string.Empty : replaceEncodedSlash ? RegexPattern.Replace(value, "/") : value, + ParameterType.UrlSegment, + encode + ) { } + +#if NET7_0_OR_GREATER + [GeneratedRegex("%2f", RegexOptions.IgnoreCase | RegexOptions.Compiled, "en-NO")] + private static partial Regex Pattern(); +#else + static Regex Pattern() => new("%2f", RegexOptions.IgnoreCase | RegexOptions.Compiled); +#endif +} \ No newline at end of file diff --git a/src/RestSharp/Polyfills/Index.cs b/src/RestSharp/Polyfills/Index.cs new file mode 100644 index 000000000..972b842e6 --- /dev/null +++ b/src/RestSharp/Polyfills/Index.cs @@ -0,0 +1,117 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if !NET +using System.Runtime.CompilerServices; + +// ReSharper disable once CheckNamespace +namespace System; + +/// Represent a type can be used to index a collection either from the start or the end. +/// +/// Index is used by the C# compiler to support the new index syntax +/// +/// int[] someArray = new int[5] { 1, 2, 3, 4, 5 } ; +/// int lastElement = someArray[^1]; // lastElement = 5 +/// +/// +readonly struct Index : IEquatable { + readonly int _value; + + /// Construct an Index using a value and indicating if the index is from the start or from the end. + /// The index value. it has to be zero or positive number. + /// Indicating if the index is from the start or from the end. + /// + /// If the Index constructed from the end, index value 1 means pointing at the last element and index value 0 means pointing at beyond last element. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Index(int value, bool fromEnd = false) { + if (value < 0) { + throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative"); + } + + if (fromEnd) + _value = ~value; + else + _value = value; + } + + // The following private constructors mainly created for perf reason to avoid the checks + Index(int value) => _value = value; + + /// Create an Index pointing at first element. + public static Index Start => new(0); + + /// Create an Index pointing at beyond last element. + public static Index End => new(~0); + + /// Create an Index from the start at the position indicated by the value. + /// The index value from the start. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Index FromStart(int value) => value < 0 ? throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative") : new Index(value); + + /// Create an Index from the end at the position indicated by the value. + /// The index value from the end. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Index FromEnd(int value) => value < 0 ? throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative") : new Index(~value); + + /// Returns the index value. + public int Value { + get { + if (_value < 0) + return ~_value; + else + return _value; + } + } + + /// Indicates whether the index is from the start or the end. + public bool IsFromEnd => _value < 0; + + /// Calculate the offset from the start using the giving collection length. + /// The length of the collection that the Index will be used with. length has to be a positive value + /// + /// For performance reason, we don't validate the input length parameter and the returned offset value against negative values. + /// we don't validate either the returned offset is greater than the input length. + /// It is expected Index will be used with collections which always have non negative length/count. If the returned offset is negative and + /// then used to index a collection will get out of range exception which will be same affect as the validation. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int GetOffset(int length) { + var offset = _value; + + if (IsFromEnd) { + // offset = length - (~value) + // offset = length + (~(~value) + 1) + // offset = length + value + 1 + + offset += length + 1; + } + + return offset; + } + + /// Indicates whether the current Index object is equal to another object of the same type. + /// An object to compare with this object + public override bool Equals(object? value) => value is Index && _value == ((Index)value)._value; + + /// Indicates whether the current Index object is equal to another Index object. + /// An object to compare with this object + public bool Equals(Index other) => _value == other._value; + + /// Returns the hash code for this instance. + public override int GetHashCode() => _value; + + /// Converts integer number to an Index. + public static implicit operator Index(int value) => FromStart(value); + + /// Converts the value of the current Index object to its equivalent string representation. + public override string ToString() { + if (IsFromEnd) + return "^" + ((uint)Value).ToString(); + + return ((uint)Value).ToString(); + } +} +#endif \ No newline at end of file diff --git a/src/RestSharp/Polyfills/Range.cs b/src/RestSharp/Polyfills/Range.cs new file mode 100644 index 000000000..49634660e --- /dev/null +++ b/src/RestSharp/Polyfills/Range.cs @@ -0,0 +1,81 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if !NET +using System.Runtime.CompilerServices; +#pragma warning disable CS3019 +// ReSharper disable once CheckNamespace +namespace System; + +/// Represent a range has start and end indexes. +/// +/// Range is used by the C# compiler to support the range syntax. +/// +/// int[] someArray = new int[5] { 1, 2, 3, 4, 5 }; +/// int[] subArray1 = someArray[0..2]; // { 1, 2 } +/// int[] subArray2 = someArray[1..^0]; // { 2, 3, 4, 5 } +/// +/// +readonly struct Range : IEquatable { + /// Represent the inclusive start index of the Range. + public Index Start { get; } + + /// Represent the exclusive end index of the Range. + public Index End { get; } + + /// Construct a Range object using the start and end indexes. + /// Represent the inclusive start index of the range. + /// Represent the exclusive end index of the range. + public Range(Index start, Index end) { + Start = start; + End = end; + } + + /// Indicates whether the current Range object is equal to another object of the same type. + /// An object to compare with this object + public override bool Equals(object? value) + => value is Range r && + r.Start.Equals(Start) && + r.End.Equals(End); + + /// Indicates whether the current Range object is equal to another Range object. + /// An object to compare with this object + public bool Equals(Range other) => other.Start.Equals(Start) && other.End.Equals(End); + + /// Returns the hash code for this instance. + public override int GetHashCode() => Start.GetHashCode() * 31 + End.GetHashCode(); + + /// Converts the value of the current Range object to its equivalent string representation. + public override string ToString() => $"{Start}..{End}"; + + /// Create a Range object starting from start index to the end of the collection. + public static Range StartAt(Index start) => new(start, Index.End); + + /// Create a Range object starting from first element in the collection to the end Index. + public static Range EndAt(Index end) => new(Index.Start, end); + + /// Create a Range object starting from first element to the end. + public static Range All => new(Index.Start, Index.End); + + /// Calculate the start offset and length of range object using a collection length. + /// The length of the collection that the range will be used with. length has to be a positive value. + /// + /// For performance reason, we don't validate the input length parameter against negative values. + /// It is expected Range will be used with collections which always have non negative length/count. + /// We validate the range is inside the length scope though. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [CLSCompliant(false)] + public (int Offset, int Length) GetOffsetAndLength(int length) { + var start = Start.GetOffset(length); + var end = End.GetOffset(length); + + if ((uint)end > (uint)length || (uint)start > (uint)end) { + throw new ArgumentOutOfRangeException(nameof(length)); + } + + return (start, end - start); + } +} +#endif \ No newline at end of file diff --git a/src/RestSharp/Polyfills/Strings.cs b/src/RestSharp/Polyfills/Strings.cs new file mode 100644 index 000000000..d26fa2076 --- /dev/null +++ b/src/RestSharp/Polyfills/Strings.cs @@ -0,0 +1,22 @@ +// 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. + +#if !NET +// ReSharper disable once CheckNamespace +namespace System; + +static class Strings { + public static string[] Split(this string str, char separator, StringSplitOptions options) => str.Split([separator], options); +} +#endif \ No newline at end of file diff --git a/src/RestSharp/Properties/AssemblyInfo.cs b/src/RestSharp/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..27ad83bc7 --- /dev/null +++ b/src/RestSharp/Properties/AssemblyInfo.cs @@ -0,0 +1,22 @@ +using System.Runtime.CompilerServices; + +[assembly: + InternalsVisibleTo( + "RestSharp.Extensions.DependencyInjection, PublicKey=0024000004800000940000000602000000240000525341310004000001000100fda57af14a288d46e3efea89617037585c4de57159cd536ca6dff792ea1d6addc665f2fccb4285413d9d44db5a1be87cb82686db200d16325ed9c42c89cd4824d8cc447f7cee2ac000924c3bceeb1b7fcb5cc1a3901785964d48ce14172001084134f4dcd9973c3776713b595443b1064bb53e2eeb924969244d354e46495e9d" + ), + InternalsVisibleTo( + "RestSharp.InteractiveTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100fda57af14a288d46e3efea89617037585c4de57159cd536ca6dff792ea1d6addc665f2fccb4285413d9d44db5a1be87cb82686db200d16325ed9c42c89cd4824d8cc447f7cee2ac000924c3bceeb1b7fcb5cc1a3901785964d48ce14172001084134f4dcd9973c3776713b595443b1064bb53e2eeb924969244d354e46495e9d" + ), + InternalsVisibleTo( + "RestSharp.Tests.Integrated, PublicKey=0024000004800000940000000602000000240000525341310004000001000100fda57af14a288d46e3efea89617037585c4de57159cd536ca6dff792ea1d6addc665f2fccb4285413d9d44db5a1be87cb82686db200d16325ed9c42c89cd4824d8cc447f7cee2ac000924c3bceeb1b7fcb5cc1a3901785964d48ce14172001084134f4dcd9973c3776713b595443b1064bb53e2eeb924969244d354e46495e9d" + ), + InternalsVisibleTo( + "RestSharp.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100fda57af14a288d46e3efea89617037585c4de57159cd536ca6dff792ea1d6addc665f2fccb4285413d9d44db5a1be87cb82686db200d16325ed9c42c89cd4824d8cc447f7cee2ac000924c3bceeb1b7fcb5cc1a3901785964d48ce14172001084134f4dcd9973c3776713b595443b1064bb53e2eeb924969244d354e46495e9d" + ), + InternalsVisibleTo( + "RestSharp.Tests.Shared, PublicKey=0024000004800000940000000602000000240000525341310004000001000100fda57af14a288d46e3efea89617037585c4de57159cd536ca6dff792ea1d6addc665f2fccb4285413d9d44db5a1be87cb82686db200d16325ed9c42c89cd4824d8cc447f7cee2ac000924c3bceeb1b7fcb5cc1a3901785964d48ce14172001084134f4dcd9973c3776713b595443b1064bb53e2eeb924969244d354e46495e9d" + ), + InternalsVisibleTo( + "RestSharp.Serializers.Xml, PublicKey=0024000004800000940000000602000000240000525341310004000001000100fda57af14a288d46e3efea89617037585c4de57159cd536ca6dff792ea1d6addc665f2fccb4285413d9d44db5a1be87cb82686db200d16325ed9c42c89cd4824d8cc447f7cee2ac000924c3bceeb1b7fcb5cc1a3901785964d48ce14172001084134f4dcd9973c3776713b595443b1064bb53e2eeb924969244d354e46495e9d" + )] +[assembly: CLSCompliant(true)] diff --git a/src/RestSharp/Properties/IsExternalInit.cs b/src/RestSharp/Properties/IsExternalInit.cs new file mode 100644 index 000000000..a77ccc3c3 --- /dev/null +++ b/src/RestSharp/Properties/IsExternalInit.cs @@ -0,0 +1,9 @@ +#if !NET +using System.ComponentModel; + +// ReSharper disable once CheckNamespace +namespace System.Runtime.CompilerServices; + +[EditorBrowsable(EditorBrowsableState.Never)] +internal class IsExternalInit{} +#endif diff --git a/src/RestSharp/Request/BodyExtensions.cs b/src/RestSharp/Request/BodyExtensions.cs new file mode 100644 index 000000000..958f97289 --- /dev/null +++ b/src/RestSharp/Request/BodyExtensions.cs @@ -0,0 +1,29 @@ +// 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. +// + +namespace RestSharp; + +using System.Diagnostics.CodeAnalysis; + +static class BodyExtensions { + extension(RestRequest request) { + public bool TryGetBodyParameter([NotNullWhen(true)] out BodyParameter? bodyParameter) { + bodyParameter = request.Parameters.FirstOrDefault(p => p.Type == ParameterType.RequestBody) as BodyParameter; + return bodyParameter != null; + } + + public bool HasFiles() => request.Files.Count > 0; + } +} diff --git a/src/RestSharp/Request/HttpRequestMessageExtensions.cs b/src/RestSharp/Request/HttpRequestMessageExtensions.cs new file mode 100644 index 000000000..923f9364f --- /dev/null +++ b/src/RestSharp/Request/HttpRequestMessageExtensions.cs @@ -0,0 +1,35 @@ +// 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. +// + +using System.Net.Http.Headers; +using RestSharp.Extensions; + +namespace RestSharp; + +static class HttpRequestMessageExtensions { + public static void AddHeaders(this HttpRequestMessage message, RequestHeaders headers) { + var headerParameters = headers.Where(x => !KnownHeaders.IsContentHeader(x.Name)); + + headerParameters.GroupBy(x => x.Name).ForEach(x => AddHeader(x, message.Headers)); + return; + + void AddHeader(IGrouping group, HttpHeaders httpHeaders) { + var parameterStringValues = group.Select(x => x.Value); + + httpHeaders.Remove(group.Key); + httpHeaders.TryAddWithoutValidation(group.Key, parameterStringValues); + } + } +} \ No newline at end of file diff --git a/src/RestSharp/Request/PropertyCache.Populator.RequestProperty.cs b/src/RestSharp/Request/PropertyCache.Populator.RequestProperty.cs new file mode 100644 index 000000000..0c6f59814 --- /dev/null +++ b/src/RestSharp/Request/PropertyCache.Populator.RequestProperty.cs @@ -0,0 +1,70 @@ +// 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. + +using System.Reflection; + +namespace RestSharp; + +static partial class PropertyCache where T : class { + sealed partial class Populator { + sealed record RequestProperty { + /// + /// Gets or sets the associated + /// with the property this object represents + /// + internal string Name { get; init; } + /// + /// Gets the associated with + /// the property this object represents + /// + internal string? Format { get; } + /// + /// Gets the associated + /// with the property this object represents + /// + internal RequestArrayQueryType ArrayQueryType { get; } + /// + /// Gets the return type of the property this object represents + /// + internal Type Type { get; } + + RequestProperty(string name, string? format, RequestArrayQueryType arrayQueryType, Type type) { + Name = name; + Format = format; + ArrayQueryType = arrayQueryType; + Type = type; + } + + /// + /// Creates a new request property representation of the provided property + /// + /// The property to turn into a request property + /// + internal static RequestProperty From(PropertyInfo property) { + var requestPropertyAttribute = + property.GetCustomAttribute() ?? + new RequestPropertyAttribute(); + + var propertyName = requestPropertyAttribute.Name ?? property.Name; + + return new( + propertyName, + requestPropertyAttribute.Format, + requestPropertyAttribute.ArrayQueryType, + property.PropertyType + ); + } + } + } +} diff --git a/src/RestSharp/Request/PropertyCache.Populator.cs b/src/RestSharp/Request/PropertyCache.Populator.cs new file mode 100644 index 000000000..4057ac999 --- /dev/null +++ b/src/RestSharp/Request/PropertyCache.Populator.cs @@ -0,0 +1,414 @@ +// 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. + +using System.Collections; +using System.ComponentModel; +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.CompilerServices; + +// ReSharper disable InconsistentNaming + +namespace RestSharp; + +static partial class PropertyCache where T : class { + sealed partial class Populator { + /// + /// Gets the name of the property this populator represents. + /// + /// + /// This corresponds to the actual property name and not the name + /// determined by + /// + internal string PropertyName { get; } + readonly Action> _populate; + + Populator(string propertyName, Action> populate) { + PropertyName = propertyName; + _populate = populate; + } + + /// + /// Populates the provided parameters collection + /// + /// The object to get parameters from + /// The parameters collection to populate + internal void Populate(T entity, ICollection parameters) => _populate(entity, parameters); + + /// + /// Creates a new populator instance from the provided property + /// + /// A public instance property from the type + /// + internal static Populator From(PropertyInfo property) { + var entity = Expression.Parameter(typeof(T)); + var callGetter = Expression.Call(entity, property.GetGetMethod()!); + + Expression convertGetterReturnToObject = + property.PropertyType.IsValueType + ? + // Values types are not automatically boxed in LINQ expressions. + // This would throw an exception. + Expression.Convert(callGetter, typeof(object)) + : + // Avoid unnecessary cast to object if property is already a reference type. + callGetter; + + // This compiles roughly to: `(T entity) => (object)entity.get_Property()`, + // where `.GetProperty()` is the getter. The reason we use LINQ expressions + // instead of direct calls to the MethodInfo instance is for an increase in + // performance. We can then leverage our knowledge of the type parameter provided. + + var getObject = Expression.Lambda>(convertGetterReturnToObject, entity).Compile(); + + var populate = GetPopulate(getObject, property); + + return new(property.Name, populate); + } + + static Action> GetPopulate(Func getFormattable, RequestProperty requestProperty) + => (model, parameters) => Populate(getFormattable(model), requestProperty, parameters); + + static Action> GetPopulate(Func getConvertible, RequestProperty requestProperty) + => (model, parameters) => Populate(getConvertible(model), requestProperty, parameters); + + static Action> GetPopulate(Func> getFormattables, RequestProperty requestProperty) + => requestProperty.ArrayQueryType switch { + RequestArrayQueryType.CommaSeparated => (model, parameters) => PopulateCsv(getFormattables(model), requestProperty, parameters), + RequestArrayQueryType.ArrayParameters => GetPopulateArray(getFormattables, requestProperty), + _ => (_, _) => { } + }; // Here we avoid the cost of checking if the format is CSV or Array every time by caching the result of this evaluation. + + static Action> GetPopulate(Func> getConvertibles, RequestProperty requestProperty) + => requestProperty.ArrayQueryType switch { + RequestArrayQueryType.CommaSeparated => (entity, parameters) => PopulateCsv(getConvertibles(entity), requestProperty, parameters), + RequestArrayQueryType.ArrayParameters => GetPopulateArray(getConvertibles, requestProperty), + _ => (_, _) => { } + }; // Here we avoid the cost of checking if the format is CSV or Array every time by caching the result of this evaluation. + + static Action> GetPopulate(Func getEnumerable, RequestProperty requestProperty) + => requestProperty.ArrayQueryType switch { + RequestArrayQueryType.CommaSeparated => (entity, parameters) => PopulateCsv(getEnumerable(entity), requestProperty, parameters), + RequestArrayQueryType.ArrayParameters => GetPopulateArray(getEnumerable, requestProperty), + _ => (_, _) => { } + }; // Here we avoid the cost of checking if the format is CSV or Array every time by caching the result of this evaluation. + + static Action> GetPopulate(Func getObject, RequestProperty requestProperty) + => requestProperty.ArrayQueryType switch { + RequestArrayQueryType.CommaSeparated => (entity, parameters) => PopulateCsv(getObject(entity), requestProperty, parameters), + RequestArrayQueryType.ArrayParameters => (entity, parameters) => PopulateArray(getObject(entity), requestProperty, parameters), + _ => (_, _) => { } + }; // Here we avoid the cost of checking if the format is CSV or Array every time by caching the result of this evaluation. + + static Action> GetPopulate(Func getObject, PropertyInfo property) { + var requestProperty = RequestProperty.From(property); + + // We need to use different conversion mechanisms for each return type. Simply calling `.ToString()` + // on every returned object would not take into account special cases like custom formatting, enumeration etc. + // Unchecked casts here are safe because we know the return type of `getObject` is boxed if needed. + return property.PropertyType switch { + var formattableType when typeof(IFormattable).IsAssignableFrom(formattableType) => GetPopulate( + entity => Unsafe.As(getObject(entity)), + requestProperty + ), + var convertibleType when typeof(IConvertible).IsAssignableFrom(convertibleType) => GetPopulate( + entity => Unsafe.As(getObject(entity)), + requestProperty + ), + var enumerableType when typeof(IEnumerable).IsAssignableFrom(enumerableType) => GetPopulateUnknown( + entity => Unsafe.As(getObject(entity)), + requestProperty + ), + // At this point we're not necessarily sure we can just treat this as a bare object + // and use its type converter. Even though the property itself returns an object, + // the object returned itself may need to be treated in a special way, so we check + // it as we go. + _ => GetPopulate(getObject, requestProperty) + }; + } + + static Action> GetPopulateUnknown(Func getEnumerable, RequestProperty requestProperty) { + if (GetSingleEnumeratedTypeOrNull(requestProperty.Type) is not { } enumeratedType) { + // Means we're dealing with a legacy, untyped enumerable instance. + // We can just convert it into an enumerable of objects and delegate + // conversion to string to the type converter of each enumerated item. + return GetPopulateKnown(getEnumerable, requestProperty); + } + + return enumeratedType switch { + _ when typeof(IFormattable).IsAssignableFrom(enumeratedType) => GetPopulate( + GetEnumerableOf(getEnumerable, enumeratedType), + requestProperty + ), + _ when typeof(IConvertible).IsAssignableFrom(enumeratedType) => GetPopulate( + GetEnumerableOf(getEnumerable, enumeratedType), + requestProperty + ), + // At this point we're not necessarily sure we can just treat this as an enumerable of objects. + // Since we know the actual enumerable may be a typed `IEnumerable<>` enumerating a type we're + // interested in, we do further checks to ensure the correct conversion to string is applied. + _ => GetPopulate(getEnumerable, requestProperty) + }; + } + + static Action> GetPopulateKnown(Func getEnumerable, RequestProperty requestProperty) + => requestProperty.ArrayQueryType switch { + RequestArrayQueryType.CommaSeparated => (entity, parameters) => PopulateCsvUnknown( + getEnumerable(entity), + requestProperty, + parameters + ), + RequestArrayQueryType.ArrayParameters => GetPopulateArray(getEnumerable, requestProperty), + _ => (_, _) => { } + }; // Here we avoid the cost of checking if the format is CSV or Array every time by caching the result of this evaluation. + + static Action> GetPopulateArray(Func> getFormattables, RequestProperty requestProperty) + => GetPopulateArray(getFormattables, formattable => GetStringValue(formattable, requestProperty), requestProperty); + + static Action> GetPopulateArray(Func> getConvertibles, RequestProperty requestProperty) + => GetPopulateArray(getConvertibles, GetStringValue, requestProperty); + + static Action> GetPopulateArray( + Func> getEnumerable, + Func toString, + RequestProperty requestProperty + ) where V : class { + // We do this to avoid recreating request property on each iteration. + var newRequestProperty = requestProperty with { Name = $"{requestProperty.Name}[]" }; + return (entity, parameters) => PopulateArray(getEnumerable(entity), toString, newRequestProperty, parameters); + } + + static Action> GetPopulateArray(Func getEnumerable, RequestProperty requestProperty) { + // We do this to avoid recreating request property on each iteration. + var newRequestProperty = requestProperty with { Name = $"{requestProperty.Name}[]" }; + return (entity, parameters) => PopulateArray(getEnumerable(entity), newRequestProperty, parameters); + } + + static void Populate(IFormattable formattable, RequestProperty requestProperty, ICollection parameters) + => Populate(GetStringValue(formattable, requestProperty), requestProperty, parameters); + + static void Populate(IConvertible convertible, RequestProperty requestProperty, ICollection parameters) + => Populate(GetStringValue(convertible), requestProperty, parameters); + + static void Populate(object @object, RequestProperty requestProperty, ICollection parameters) + => Populate(GetStringValueKnown(@object), requestProperty, parameters); + + static void Populate(string? stringValue, RequestProperty requestProperty, ICollection parameters) { + var parameter = new GetOrPostParameter(requestProperty.Name, stringValue); + parameters.Add(parameter); + } + + static void PopulateCsv(IEnumerable formattables, RequestProperty requestProperty, ICollection parameters) + => PopulateCsv(formattables, formattable => GetStringValue(formattable, requestProperty), requestProperty, parameters); + + static void PopulateCsv(IEnumerable convertibles, RequestProperty requestProperty, ICollection parameters) + => PopulateCsv(convertibles, GetStringValue, requestProperty, parameters); + + static void PopulateCsv(IEnumerable objects, RequestProperty requestProperty, ICollection parameters) + => PopulateCsv(objects, @object => GetStringValueUnknown(@object, requestProperty), requestProperty, parameters); + + static void PopulateCsv(IEnumerable enumerable, RequestProperty requestProperty, ICollection parameters) { + switch (enumerable) { + case IEnumerable formattables: + PopulateCsv(formattables, requestProperty, parameters); + break; + case IEnumerable convertibles: + PopulateCsv(convertibles, requestProperty, parameters); + break; + case IEnumerable objects: + PopulateCsv(objects, requestProperty, parameters); + break; + default: + PopulateCsvUnknown(enumerable, requestProperty, parameters); + break; + } + } + + static void PopulateCsv( + IEnumerable enumerable, + Func toString, + RequestProperty requestProperty, + ICollection parameters + ) where V : class { +#if NETCOREAPP2_0_OR_GREATER + const char csvSeparator = ','; +#else + const string csvSeparator = ","; +#endif + var formattedStrings = enumerable.Select(toString); + var csv = string.Join(csvSeparator, formattedStrings); + Populate(csv, requestProperty, parameters); + } + + static void PopulateCsv(object @object, RequestProperty requestProperty, ICollection parameters) { + switch (@object) { + case IFormattable formattable: + Populate(formattable, requestProperty, parameters); + break; + case IConvertible convertible: + Populate(convertible, requestProperty, parameters); + break; + case IEnumerable enumerable: + PopulateCsv(enumerable, requestProperty, parameters); + break; + default: + // At this point it's safe to assume we can delegate + // to the type converter. + Populate(@object, requestProperty, parameters); + break; + } + } + + static void PopulateCsvUnknown(IEnumerable enumerable, RequestProperty requestProperty, ICollection parameters) { + if (GetSingleEnumeratedTypeOrNull(enumerable.GetType()) is not { } enumeratedType) { + // Means we're dealing with a legacy, untyped enumerable instance. + // We can just convert it into an enumerable of objects and delegate + // conversion to string to the type converter of each enumerated item. + PopulateCsvKnown(enumerable, requestProperty, parameters); + return; + } + + switch (enumeratedType) { + case var _ when typeof(IFormattable).IsAssignableFrom(enumeratedType): + PopulateCsv(enumerable.Cast(), requestProperty, parameters); + break; + case var _ when typeof(IConvertible).IsAssignableFrom(enumeratedType): + PopulateCsv(enumerable.Cast(), requestProperty, parameters); + break; + default: + PopulateCsvKnown(enumerable, requestProperty, parameters); + break; + } + } + + static void PopulateCsvKnown(IEnumerable enumerable, RequestProperty requestProperty, ICollection parameters) + => PopulateCsv(enumerable.Cast(), requestProperty, parameters); + + static void PopulateArray(IEnumerable formattables, RequestProperty requestProperty, ICollection parameters) + => PopulateArray(formattables, formattable => GetStringValue(formattable, requestProperty), requestProperty, parameters); + + static void PopulateArray(IEnumerable convertibles, RequestProperty requestProperty, ICollection parameters) + => PopulateArray(convertibles, GetStringValue, requestProperty, parameters); + + static void PopulateArray(IEnumerable objects, RequestProperty requestProperty, ICollection parameters) + => PopulateArray(objects, @object => GetStringValueUnknown(@object, requestProperty), requestProperty, parameters); + + static void PopulateArray(IEnumerable enumerable, RequestProperty requestProperty, ICollection parameters) { + switch (enumerable) { + case IEnumerable formattables: + PopulateArray(formattables, requestProperty, parameters); + break; + case IEnumerable convertibles: + PopulateArray(convertibles, requestProperty, parameters); + break; + case IEnumerable objects: + PopulateArray(objects, requestProperty, parameters); + break; + default: + PopulateArrayUnknown(enumerable, requestProperty, parameters); + break; + } + } + + static void PopulateArray( + IEnumerable enumerable, + Func toString, + RequestProperty requestProperty, + ICollection parameters + ) where V : class { + var values = enumerable.Select(toString); + + foreach (var value in values) { + Populate(value, requestProperty, parameters); + } + } + + static void PopulateArray(object @object, RequestProperty requestProperty, ICollection parameters) { + switch (@object) { + case IFormattable formattable: + Populate(formattable, requestProperty, parameters); + break; + case IConvertible convertible: + Populate(convertible, requestProperty, parameters); + break; + case IEnumerable enumerable: + // We do this to avoid recreating request property on each iteration. + requestProperty = requestProperty with { Name = $"{requestProperty.Name}[]" }; + PopulateArray(enumerable, requestProperty, parameters); + break; + default: + // At this point it's safe to assume we can delegate + // to the type converter. + Populate(@object, requestProperty, parameters); + break; + } + } + + static void PopulateArrayUnknown(IEnumerable enumerable, RequestProperty requestProperty, ICollection parameters) { + if (GetSingleEnumeratedTypeOrNull(enumerable.GetType()) is not { } enumeratedType) { + // Means we're dealing with a legacy, untyped enumerable instance. + // We can just convert it into an enumerable of objects and delegate + // conversion to string to the type converter of each enumerated item. + PopulateArrayKnown(enumerable, requestProperty, parameters); + return; + } + + switch (enumeratedType) { + case var _ when typeof(IFormattable).IsAssignableFrom(enumeratedType): + PopulateArray(enumerable.Cast(), requestProperty, parameters); + break; + case var _ when typeof(IConvertible).IsAssignableFrom(enumeratedType): + PopulateArray(enumerable.Cast(), requestProperty, parameters); + break; + default: + PopulateArrayKnown(enumerable, requestProperty, parameters); + break; + } + } + + static void PopulateArrayKnown(IEnumerable enumerable, RequestProperty requestProperty, ICollection parameters) + => PopulateArray(enumerable.Cast(), requestProperty, parameters); + + static string GetStringValue(IFormattable formattable, RequestProperty requestProperty) => formattable.ToString(requestProperty.Format, null); + + static string GetStringValue(IConvertible convertible) => convertible.ToString(null); + + static string? GetStringValueKnown(object @object) => TypeDescriptor.GetConverter(@object).ConvertToString(@object); + + static string? GetStringValueUnknown(object @object, RequestProperty requestProperty) + => @object switch { + IFormattable formattable => GetStringValue(formattable, requestProperty), + IConvertible convertible => GetStringValue(convertible), + _ => GetStringValueKnown(@object) + }; + + static Func> GetEnumerableOf(Func getEnumerable, Type enumeratedType) where V : class + => enumeratedType.IsValueType ? entity => getEnumerable(entity).Cast() : entity => Unsafe.As>(getEnumerable(entity)); + + static Type? GetSingleEnumeratedTypeOrNull(Type enumerableType) { + // Get all IEnumerable<> interfaces this type implements. + var enumerableInterfaces = + enumerableType + .GetInterfaces() + .Where(@interface => @interface.IsGenericType) + .Where(@interface => @interface.GetGenericTypeDefinition() == typeof(IEnumerable<>)) + .ToArray(); + + // If this type implements `IEnumerable<>` multiple times with different type parameters + // we cannot pick which implementation to "believe", so we treat the whole thing as a bare, + // untyped `IEnumerable`. + return enumerableInterfaces.Length == 1 ? enumerableInterfaces[0].GetGenericArguments()[0] : null; + } + } +} \ No newline at end of file diff --git a/src/RestSharp/Request/PropertyCache.cs b/src/RestSharp/Request/PropertyCache.cs new file mode 100644 index 000000000..1b15e7faf --- /dev/null +++ b/src/RestSharp/Request/PropertyCache.cs @@ -0,0 +1,75 @@ +// 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. + +using System.Reflection; + +namespace RestSharp; + +static partial class PropertyCache where T : class { + static readonly IReadOnlyCollection Populators = + typeof(T) + .GetProperties(BindingFlags.Public | BindingFlags.Instance) + // We need to ensure the property does not return a ref struct + // since reflection and LINQ expressions do not play well with + // them. All bets are off, so let's just ignore them. +#if NET + .Where(property => !property.PropertyType.IsByRefLike) +#else + // Since `IsByRefLikeAttribute` is generated at compile time, each assembly + // may have its own definition of the attribute, so we must compare by full name + // instead of type. + .Where(property => property.PropertyType.GetCustomAttributes().Select(attribute => attribute.GetType().FullName).All(attributeName => attributeName != "System.Runtime.CompilerServices.IsByRefLikeAttribute")) +#endif + .Select(Populator.From) + .ToArray(); + + /// + /// Gets parameters from the provided object + /// + /// The object from which to get the parameters + /// Properties to include, or nothing to include everything. The array will be sorted. + /// + internal static IEnumerable GetParameters(T entity, params string[] includedProperties) { + if (includedProperties.Length == 0) { + return GetParameters(entity); + } + + Array.Sort(includedProperties); // Otherwise binary search is unsafe. + + // Get only populators found in `includedProperties`. + + var populators = Populators.Where(populator => Array.BinarySearch(includedProperties, populator.PropertyName) >= 0); + return GetParameters(entity, populators); + } + + /// + /// Gets parameters from the provided object + /// + /// The object from which to get the parameters + /// + internal static IEnumerable GetParameters(T entity) => GetParameters(entity, Populators); + + static List GetParameters(T entity, IEnumerable populators) { + var parameters = new List(capacity: Populators.Count); + + foreach (var populator in populators) { + // Each populator may return one or more parameters, + // so they take temporary ownership of the list in order + // to populate it with its own set of parameters. + populator.Populate(entity, parameters); + } + + return parameters; + } +} diff --git a/src/RestSharp/Request/RequestContent.cs b/src/RestSharp/Request/RequestContent.cs new file mode 100644 index 000000000..03af0bdcf --- /dev/null +++ b/src/RestSharp/Request/RequestContent.cs @@ -0,0 +1,233 @@ +// 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. + +using System.Net.Http.Headers; +using System.Runtime.Serialization; +using RestSharp.Extensions; +using static RestSharp.KnownHeaders; + +// ReSharper disable InvertIf +// ReSharper disable SuggestBaseTypeForParameter + +namespace RestSharp; + +class RequestContent(IRestClient client, RestRequest request) : IDisposable { + readonly List _streams = []; + readonly ParametersCollection _parameters = new RequestParameters(request.Parameters.Union(client.DefaultParameters)); + + HttpContent? Content { get; set; } + + public HttpContent BuildContent() { + var postParameters = _parameters.GetContentParameters(request.Method).ToArray(); + var postParametersExists = postParameters.Length > 0; + var bodyParametersExists = request.TryGetBodyParameter(out var bodyParameter); + var filesExists = request.Files.Count != 0; + + if (request.HasFiles() || + BodyShouldBeMultipartForm(bodyParameter) || + filesExists || + request.AlwaysMultipartFormData) { + Content = CreateMultipartFormDataContent(); + } + + if (filesExists) AddFiles(); + + if (bodyParametersExists) AddBody(postParametersExists, bodyParameter!); + + if (postParametersExists) AddPostParameters(postParameters); + + AddHeaders(); + + return Content!; + } + + void AddFiles() { + // File uploading without multipart/form-data + if (request is { AlwaysSingleFileAsContent: true, Files.Count: 1 }) { + var fileParameter = request.Files.First(); + Content?.Dispose(); + Content = ToStreamContent(fileParameter); + return; + } + + var mpContent = Content as MultipartFormDataContent; + foreach (var fileParameter in request.Files) mpContent!.Add(ToStreamContent(fileParameter)); + } + + StreamContent ToStreamContent(FileParameter fileParameter) { + var stream = fileParameter.GetFile(); + _streams.Add(stream); + var streamContent = new StreamContent(stream); + + streamContent.Headers.ContentType = fileParameter.ContentType.AsMediaTypeHeaderValue; + + var dispositionHeader = fileParameter.Options.DisableFilenameEncoding + ? ContentDispositionHeaderValue.Parse($"form-data; name=\"{fileParameter.Name}\"; filename=\"{fileParameter.FileName}\"") + : new("form-data") { Name = $"\"{fileParameter.Name}\"", FileName = $"\"{fileParameter.FileName}\"" }; + if (!fileParameter.Options.DisableFilenameStar) dispositionHeader.FileNameStar = fileParameter.FileName; + streamContent.Headers.ContentDisposition = dispositionHeader; + + return streamContent; + } + + HttpContent Serialize(BodyParameter body) { + return body.DataFormat switch { + DataFormat.None => new StringContent(body.Value!.ToString()!, client.Options.Encoding, body.ContentType.Value), + DataFormat.Binary => GetBinary(), + _ => GetSerialized() + }; + + HttpContent GetBinary() { + var byteContent = new ByteArrayContent((body.Value as byte[])!); + byteContent.Headers.ContentType = body.ContentType.AsMediaTypeHeaderValue; + + if (body.ContentEncoding != null) { + byteContent.Headers.ContentEncoding.Clear(); + byteContent.Headers.ContentEncoding.Add(body.ContentEncoding); + } + + return byteContent; + } + + HttpContent GetSerialized() { + var serializer = client.Serializers.GetSerializer(body.DataFormat); + var content = serializer.Serialize(body); + + if (content == null) throw new SerializationException("Request body serialized to null"); + + var contentType = body.ContentType.Or(serializer.Serializer.ContentType); + + return new StringContent(content, client.Options.Encoding, contentType.Value); + } + } + + static bool BodyShouldBeMultipartForm(BodyParameter? bodyParameter) { + if (bodyParameter == null) return false; + + var bodyContentType = bodyParameter.ContentType.OrValue(bodyParameter.Name); + return bodyParameter.Name.IsNotEmpty() && bodyParameter.Name != bodyContentType; + } + + string GetOrSetFormBoundary() => request.FormBoundary ?? (request.FormBoundary = Guid.NewGuid().ToString()); + + MultipartFormDataContent CreateMultipartFormDataContent() { + var boundary = GetOrSetFormBoundary(); + var mpContent = new MultipartFormDataContent(boundary); + var contentType = new MediaTypeHeaderValue("multipart/form-data"); + contentType.Parameters.Add(new(nameof(boundary), GetBoundary(boundary, request.MultipartFormQuoteBoundary))); + mpContent.Headers.ContentType = contentType; + return mpContent; + } + + void AddBody(bool hasPostParameters, BodyParameter bodyParameter) { + var bodyContent = Serialize(bodyParameter); + + // we need to send the body + if (hasPostParameters || request.HasFiles() || BodyShouldBeMultipartForm(bodyParameter) || request.AlwaysMultipartFormData) { + // here we must use multipart form data + var mpContent = Content as MultipartFormDataContent ?? CreateMultipartFormDataContent(); + var ct = bodyContent.Headers.ContentType?.MediaType; + var name = bodyParameter.Name.IsEmpty() ? ct : bodyParameter.Name; + + if (name.IsEmpty()) + mpContent.Add(bodyContent); + else + mpContent.Add(bodyContent, name); + Content = mpContent; + } + else { + // we don't have parameters, only the body + Content = bodyContent; + } + + if (client.Options.DisableCharset) { + Content.Headers.ContentType!.CharSet = ""; + } + } + + void AddPostParameters(GetOrPostParameter[] postParameters) { + if (postParameters.Length == 0) return; + + if (Content is MultipartFormDataContent mpContent) { + // we got the multipart form already instantiated, just add parameters to it + foreach (var postParameter in postParameters) { + var parameterName = postParameter.Name!; + + mpContent.Add( + new StringContent(postParameter.Value?.ToString() ?? string.Empty, client.Options.Encoding, postParameter.ContentType.Value), + request.MultipartFormQuoteParameters ? $"\"{parameterName}\"" : parameterName + ); + } + } + else { + var encodedItems = postParameters.Select(x => $"{x.Name!.UrlEncode()}={x.Value?.ToString()?.UrlEncode() ?? string.Empty}"); + var encodedContent = new StringContent(encodedItems.JoinToString("&"), client.Options.Encoding, ContentType.FormUrlEncoded.Value); + + if (client.Options.DisableCharset) { + encodedContent.Headers.ContentType!.CharSet = ""; + } + + Content = encodedContent; + } + } + + static string GetBoundary(string boundary, bool quote) => quote ? $"\"{boundary}\"" : boundary; + + void AddHeaders() { + var contentHeaders = _parameters + .GetParameters() + .Where(x => IsContentHeader(x.Name!)) + .ToArray(); + + if (contentHeaders.Length > 0 && Content == null) { + // We need some content to add content headers to it, so if necessary, we'll add empty content + Content = new StringContent(""); + } + + contentHeaders.ForEach(AddHeader); + return; + + void AddHeader(HeaderParameter parameter) { + var parameterStringValue = parameter.Value!.ToString(); + + var value = parameter.Name switch { + KnownHeaders.ContentType => GetContentTypeHeader(Ensure.NotNull(parameterStringValue, nameof(parameter))), + _ => parameterStringValue + }; + var pName = Ensure.NotNull(parameter.Name, nameof(parameter.Name)); + ReplaceHeader(pName, value); + } + + string GetContentTypeHeader(string contentType) + => Content is MultipartFormDataContent + ? $"{contentType}; boundary={GetBoundary(GetOrSetFormBoundary(), request.MultipartFormQuoteBoundary)}" + : contentType; + } + + void ReplaceHeader(string name, string? value) { + Content!.Headers.Remove(name); + Content!.Headers.TryAddWithoutValidation(name, value); + } + + public void Dispose() { + _streams.ForEach(x => x.Dispose()); + + try { + Content?.Dispose(); + } + catch (Exception e) when (e is ObjectDisposedException or NullReferenceException) { + // Already disposed + } + } +} \ No newline at end of file diff --git a/src/RestSharp/Request/RequestHeaders.cs b/src/RestSharp/Request/RequestHeaders.cs new file mode 100644 index 000000000..10677d6e3 --- /dev/null +++ b/src/RestSharp/Request/RequestHeaders.cs @@ -0,0 +1,59 @@ +// 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. +// + +// ReSharper disable InvertIf + +using RestSharp.Extensions; + +namespace RestSharp; + +class RequestHeaders : ParametersCollection { + public RequestHeaders AddHeaders(ParametersCollection parameters) { + Parameters.AddRange(parameters.GetParameters()); + return this; + } + + // Add Accept header based on registered deserializers if the caller has set none. + public RequestHeaders AddAcceptHeader(string[] acceptedContentTypes) { + if (TryFind(KnownHeaders.Accept) == null) { + var accepts = acceptedContentTypes.JoinToString(", "); + Parameters.Add(new(KnownHeaders.Accept, accepts)); + } + + return this; + } + + // Add Cookie header from the cookie container + public RequestHeaders AddCookieHeaders(Uri uri, CookieContainer? cookieContainer) { + if (cookieContainer == null) return this; + + var cookies = cookieContainer.GetCookieHeader(uri); + + if (string.IsNullOrWhiteSpace(cookies)) return this; + + var newCookies = SplitHeader(cookies); + var existing = GetParameters().FirstOrDefault(x => x.Name == KnownHeaders.Cookie); + + if (existing?.Value != null) { + newCookies = newCookies.Union(SplitHeader(existing.Value!)); + } + + Parameters.Add(new(KnownHeaders.Cookie, string.Join("; ", newCookies))); + + return this; + + IEnumerable SplitHeader(string header) => header.Split(';').Select(x => x.Trim()); + } +} diff --git a/src/RestSharp/Request/RestRequest.cs b/src/RestSharp/Request/RestRequest.cs new file mode 100644 index 000000000..786f11304 --- /dev/null +++ b/src/RestSharp/Request/RestRequest.cs @@ -0,0 +1,258 @@ +// 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. + +using System.Net.Http.Headers; +using RestSharp.Authenticators; +using RestSharp.Extensions; +using RestSharp.Interceptors; + +// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global +// ReSharper disable UnusedAutoPropertyAccessor.Global + +namespace RestSharp; + +/// +/// Container for data used to make requests +/// +public class RestRequest { + /// + /// Default constructor + /// + public RestRequest() => Method = Method.Get; + + /// + /// Constructor for a rest request to a relative resource URL and optional method + /// + /// Resource to use + /// Method to use (defaults to Method.Get> + public RestRequest(string? resource, Method method = Method.Get) : this() { + Resource = resource ?? ""; + Method = method; + + if (string.IsNullOrWhiteSpace(resource)) return; + + var queryStringStart = Resource.IndexOf('?'); + + if (queryStringStart < 0 || Resource.IndexOf('=') <= queryStringStart) return; + + var queryParams = ParseQuery(Resource[(queryStringStart + 1)..]); + Resource = Resource[..queryStringStart]; + + foreach (var param in queryParams) this.AddQueryParameter(param.Key, param.Value, false); + + return; + + static IEnumerable> ParseQuery(string query) + => query.Split('&', StringSplitOptions.RemoveEmptyEntries) + .Select( + x => { + var position = x.IndexOf('='); + + return position > 0 ? new KeyValuePair(x[..position], x[(position + 1)..]) : new(x, null); + } + ); + } + + /// + /// Constructor for a rest request to a specific resource Uri and optional method + /// + /// Resource Uri to use + /// Method to use (defaults to Method.Get> + public RestRequest(Uri resource, Method method = Method.Get) + : this(resource.IsAbsoluteUri ? resource.AbsoluteUri : resource.OriginalString, method) { } + + readonly List _files = []; + + /// + /// Always send a multipart/form-data request - even when no Files are present. + /// + public bool AlwaysMultipartFormData { get; set; } + + /// + /// Always send a file as request content without multipart/form-data request - even when the request contains only one file parameter + /// + public bool AlwaysSingleFileAsContent { get; set; } + + /// + /// When set to true, parameter values in a multipart form data requests will be enclosed in + /// quotation marks. Default is false. Enable it if the remote endpoint requires parameters + /// to be in quotes (for example, FreshDesk API). + /// + public bool MultipartFormQuoteParameters { get; set; } + + /// + /// When set to true, the form boundary part of the content type will be enclosed in + /// quotation marks. Default is true. + /// + [PublicAPI] + public bool MultipartFormQuoteBoundary { get; set; } = true; + + /// + /// Overrides the default (random) form boundary + /// + public string? FormBoundary { get; set; } + + /// + /// Container of all HTTP parameters to be passed with the request. + /// See AddParameter() for explanation of the types of parameters that can be passed + /// + public RequestParameters Parameters { get; } = new(); + + /// + /// Optional cookie container to use for the request. If not set, cookies are not passed. + /// + public CookieContainer? CookieContainer { get; set; } + + /// + /// Request-level authenticator. It will be used if set, otherwise RestClient.Authenticator will be used. + /// + public IAuthenticator? Authenticator { get; set; } + + /// + /// Container of all the files to be uploaded with the request. + /// + public IReadOnlyCollection Files => _files.AsReadOnly(); + + /// + /// Determines what HTTP method to use for this request. Supported methods: GET, POST, PUT, DELETE, HEAD, OPTIONS + /// Default is GET + /// + public Method Method { get; set; } + + /// + /// Custom request timeout + /// + public TimeSpan? Timeout { get; set; } + + /// + /// The Resource URL to make the request against. + /// Tokens are substituted with UrlSegment parameters and match by name. + /// Should not include the scheme or domain. Do not include leading slash. + /// Combined with RestClient.BaseUrl to assemble final URL: + /// {BaseUrl}/{Resource} (BaseUrl is scheme + domain, e.g. http://example.com) + /// + /// + /// // example for url token replacement + /// request.Resource = "Products/{ProductId}"; + /// request.AddParameter("ProductId", 123, ParameterType.UrlSegment); + /// + [PublicAPI] + public string Resource { get; set; } = ""; + + /// + /// Serializer to use when writing request bodies. + /// + public DataFormat RequestFormat { get; set; } + + /// + /// Used by the default deserializers to determine where to start deserializing from. + /// Can be used to skip container or root elements that do not have corresponding deserialization targets. + /// + public string? RootElement { get; set; } + + /// + /// HTTP version for the request. Default is Version11. + /// + public Version Version { get; set; } = HttpVersion.Version11; + + /// + /// When supplied, the function will be called before calling the deserializer + /// + [Obsolete("Use Interceptors instead")] + public Action? OnBeforeDeserialization { get; set; } + + /// + /// When supplied, the function will be called before making a request + /// + [Obsolete("Use Interceptors instead")] + public Func? OnBeforeRequest { get; set; } + + /// + /// When supplied, the function will be called after the request is complete + /// + [Obsolete("Use Interceptors instead")] + public Func? OnAfterRequest { get; set; } + + internal void IncreaseNumberOfAttempts() => Attempts++; + + /// + /// The number of attempts that were made to send this request + /// + /// + /// This number is incremented each time the RestClient sends the request. + /// + [PublicAPI] + public int Attempts { get; private set; } + + /// + /// Completion option for + /// + public HttpCompletionOption CompletionOption { get; set; } = HttpCompletionOption.ResponseContentRead; + + /// + /// Cache policy to be used for requests using + /// + public CacheControlHeaderValue? CachePolicy { get; set; } + + /// + /// Set this to write response to Stream rather than reading into memory. + /// + public Func? ResponseWriter { + get; + set { + if (AdvancedResponseWriter != null) { + throw new ArgumentException("AdvancedResponseWriter is not null. Only one response writer can be used."); + } + + field = value; + } + } + + /// + /// Set this to handle the response stream yourself, based on the response details + /// + public Func? AdvancedResponseWriter { + get; + set { + if (ResponseWriter != null) { + throw new ArgumentException("ResponseWriter is not null. Only one response writer can be used."); + } + + field = value; + } + } + + /// + /// Request-level interceptors. Will be combined with client-level interceptors if set. + /// + public List? Interceptors { get; set; } + + /// + /// Adds a parameter object to the request parameters + /// + /// Parameter to add + /// + public RestRequest AddParameter(Parameter parameter) => this.With(x => x.Parameters.AddParameter(parameter)); + + /// + /// Removes a parameter object from the request parameters + /// + /// Parameter to remove + public RestRequest RemoveParameter(Parameter parameter) { + Parameters.RemoveParameter(parameter); + return this; + } + + internal RestRequest AddFile(FileParameter file) => this.With(x => x._files.Add(file)); +} \ No newline at end of file diff --git a/src/RestSharp/Request/RestRequestExtensions.Body.cs b/src/RestSharp/Request/RestRequestExtensions.Body.cs new file mode 100644 index 000000000..39e0a1cde --- /dev/null +++ b/src/RestSharp/Request/RestRequestExtensions.Body.cs @@ -0,0 +1,118 @@ +// 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. + +namespace RestSharp; + +public static partial class RestRequestExtensions { + /// Request instance + extension(RestRequest request) { + /// + /// Adds a body parameter to the request + /// + /// Object to be used as the request body, or string for plain content + /// Optional: content type + /// + /// Thrown if request body type cannot be resolved + /// This method will try to figure out the right content type based on the request data format and the provided content type + public RestRequest AddBody(object obj, ContentType? contentType = null) { + if (contentType == null) { + return request.RequestFormat switch { + DataFormat.Json => request.AddJsonBody(obj, contentType), + DataFormat.Xml => request.AddXmlBody(obj, contentType), + DataFormat.Binary => request.AddParameter(new BodyParameter("", obj, ContentType.Binary)), + _ => request.AddParameter(new BodyParameter("", obj.ToString()!, ContentType.Plain)) + }; + } + + return + obj is string str ? request.AddStringBody(str, contentType) : + obj is byte[] bytes ? request.AddParameter(new BodyParameter("", bytes, contentType, DataFormat.Binary)) : + contentType.Value.Contains("xml") ? request.AddXmlBody(obj, contentType) : + contentType.Value.Contains("json") ? request.AddJsonBody(obj, contentType) : + throw new ArgumentException("Non-string body found with unsupported content type", nameof(obj)); + } + + /// + /// Adds a string body and figures out the content type from the data format specified. You can, for example, add a JSON string + /// using this method as request body, using DataFormat.Json/> + /// + /// String body + /// for the content + /// + public RestRequest AddStringBody(string body, DataFormat dataFormat) { + var contentType = ContentType.FromDataFormat(dataFormat); + request.RequestFormat = dataFormat; + return request.AddParameter(new BodyParameter("", body, contentType)); + } + + /// + /// Adds a string body to the request using the specified content type. + /// + /// String body + /// Content type of the body + /// + public RestRequest AddStringBody(string body, ContentType contentType) + => request.AddParameter(new BodyParameter(body, Ensure.NotNull(contentType, nameof(contentType)))); + + /// + /// Adds a JSON body parameter to the request from a string + /// + /// Force serialize the top-level string + /// Optional: content type. Default is ContentType.Json + /// JSON string to be used as a body + /// + public RestRequest AddJsonBody(string jsonString, bool forceSerialize, ContentType? contentType = null) { + request.RequestFormat = DataFormat.Json; + + return !forceSerialize + ? request.AddStringBody(jsonString, DataFormat.Json) + : request.AddParameter(new JsonParameter(jsonString, contentType)); + } + + /// + /// Adds a JSON body parameter to the request + /// + /// Object that will be serialized to JSON + /// Optional: content type. Default is ContentType.Json + /// + public RestRequest AddJsonBody(T obj, ContentType? contentType = null) where T : class { + request.RequestFormat = DataFormat.Json; + + return obj is string str + ? request.AddStringBody(str, DataFormat.Json) + : request.AddParameter(new JsonParameter(obj, contentType)); + } + + /// + /// Adds an XML body parameter to the request + /// + /// Object that will be serialized to XML + /// Optional: content type. Default is ContentType.Xml + /// Optional: XML namespace + /// + public RestRequest AddXmlBody(T obj, ContentType? contentType = null, string xmlNamespace = "") + where T : class { + request.RequestFormat = DataFormat.Xml; + + return obj is string str + ? request.AddStringBody(str, DataFormat.Xml) + : request.AddParameter(new XmlParameter(obj, xmlNamespace, contentType)); + } + + RestRequest AddBodyParameter(string? name, object value) + => name != null && name.Contains('/') + ? request.AddBody(value, name) + : request.AddParameter(new BodyParameter(name, value, ContentType.Plain)); + } +} \ No newline at end of file diff --git a/src/RestSharp/Request/RestRequestExtensions.File.cs b/src/RestSharp/Request/RestRequestExtensions.File.cs new file mode 100644 index 000000000..4674338ac --- /dev/null +++ b/src/RestSharp/Request/RestRequestExtensions.File.cs @@ -0,0 +1,88 @@ +// 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. + +namespace RestSharp; + +public static partial class RestRequestExtensions { + /// Request instance + extension(RestRequest request) { + /// + /// Adds a file parameter to the request body. The file will be read from disk as a stream. + /// + /// Parameter name + /// Full path to the file + /// Optional: content type + /// File parameter header options + /// + public RestRequest AddFile( + string name, + string path, + ContentType? contentType = null, + FileParameterOptions? options = null + ) + => request.AddFile(FileParameter.FromFile(path, name, contentType, options)); + + /// + /// Adds bytes to the request as file attachment + /// + /// Parameter name + /// File content as bytes + /// File name + /// Optional: content type. Default is "application/octet-stream" + /// File parameter header options + /// + public RestRequest AddFile( + string name, + byte[] bytes, + string fileName, + ContentType? contentType = null, + FileParameterOptions? options = null + ) + => request.AddFile(FileParameter.Create(name, bytes, fileName, contentType, options)); + + /// + /// Adds a file attachment to the request, where the file content will be retrieved from a given stream + /// + /// Parameter name + /// Function that returns a stream with the file content + /// File name + /// Optional: content type. Default is "application/octet-stream" + /// File parameter header options + /// + public RestRequest AddFile( + string name, + Func getFile, + string fileName, + ContentType? contentType = null, + FileParameterOptions? options = null + ) + => request.AddFile(FileParameter.Create(name, getFile, fileName, contentType, options)); + + internal void ValidateParameters() { + if (!request.AlwaysSingleFileAsContent) return; + + var postParametersExists = request.Parameters.GetContentParameters(request.Method).Any(); + var bodyParametersExists = request.Parameters.Any(p => p.Type == ParameterType.RequestBody); + + if (request.AlwaysMultipartFormData) + throw new ArgumentException("Failed to put file as content because flag AlwaysMultipartFormData is enabled"); + + if (postParametersExists) + throw new ArgumentException("Failed to put file as content because POST parameters were added"); + + if (bodyParametersExists) + throw new ArgumentException("Failed to put file as content because body parameters were added"); + } + } +} \ No newline at end of file diff --git a/src/RestSharp/Request/RestRequestExtensions.Headers.cs b/src/RestSharp/Request/RestRequestExtensions.Headers.cs new file mode 100644 index 000000000..2838db65f --- /dev/null +++ b/src/RestSharp/Request/RestRequestExtensions.Headers.cs @@ -0,0 +1,114 @@ +// 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. + +namespace RestSharp; + +public static partial class RestRequestExtensions { + /// Request instance + extension(RestRequest request) { + /// + /// Adds a header to the request. RestSharp will try to separate request and content headers when calling the resource. + /// + /// Header name + /// Header values + /// + public RestRequest AddHeader(string name, string[] values) { + foreach (var value in values) { + AddHeader(request, name, value); + } + + return request; + } + + /// + /// Adds a header to the request. RestSharp will try to separate request and content headers when calling the resource. + /// + /// Header name + /// Header value + /// + public RestRequest AddHeader(string name, string value) + => request.AddParameter(new HeaderParameter(name, value)); + + /// + /// Adds a header to the request. RestSharp will try to separate request and content headers when calling the resource. + /// + /// Header name + /// Header value + /// + public RestRequest AddHeader(string name, T value) where T : struct + => request.AddHeader(name, Ensure.NotNull(value.ToString(), nameof(value))); + + /// + /// Adds or updates the request header. RestSharp will try to separate request and content headers when calling the resource. + /// The existing header with the same name will be replaced. + /// + /// Header name + /// Header value + /// + public RestRequest AddOrUpdateHeader(string name, string value) + => request.AddOrUpdateParameter(new HeaderParameter(name, value)); + + /// + /// Adds or updates the request header. RestSharp will try to separate request and content headers when calling the resource. + /// The existing header with the same name will be replaced. + /// + /// Header name + /// Header value + /// + public RestRequest AddOrUpdateHeader(string name, T value) where T : struct + => request.AddOrUpdateHeader(name, Ensure.NotNull(value.ToString(), nameof(value))); + + /// + /// Adds multiple headers to the request, using the key-value pairs provided. + /// + /// Collection of key-value pairs, where key will be used as header name, and value as header value + /// + public RestRequest AddHeaders(ICollection> headers) { + CheckAndThrowsDuplicateKeys(headers); + + foreach (var header in headers) { + request.AddHeader(header.Key, header.Value); + } + + return request; + } + + /// + /// Adds or updates multiple headers to the request, using the key-value pairs provided. Existing headers with the same name will be replaced. + /// + /// Collection of key-value pairs, where key will be used as header name, and value as header value + /// + public RestRequest AddOrUpdateHeaders(ICollection> headers) { + CheckAndThrowsDuplicateKeys(headers); + + foreach (var pair in headers) { + request.AddOrUpdateHeader(pair.Key, pair.Value); + } + + return request; + } + } + + static void CheckAndThrowsDuplicateKeys(ICollection> headers) { + var duplicateKeys = headers + .GroupBy(pair => pair.Key.ToUpperInvariant()) + .Where(group => group.Count() > 1) + .Select(group => group.Key) + .ToList(); + + if (duplicateKeys.Count > 0) { + throw new ArgumentException($"Duplicate header names exist: {string.Join(", ", duplicateKeys)}"); + } + } +} \ No newline at end of file diff --git a/src/RestSharp/Request/RestRequestExtensions.Object.cs b/src/RestSharp/Request/RestRequestExtensions.Object.cs new file mode 100644 index 000000000..fbfd2715f --- /dev/null +++ b/src/RestSharp/Request/RestRequestExtensions.Object.cs @@ -0,0 +1,65 @@ +// 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. + +namespace RestSharp; + +public static partial class RestRequestExtensions { + /// Request instance + extension(RestRequest request) { + /// + /// Gets object properties and adds each property as a form data parameter + /// + /// Object to add as form data + /// Properties to include, or nothing to include everything. The array will be sorted. + /// + public RestRequest AddObject(T obj, params string[] includedProperties) where T : class { + var props = obj.GetProperties(includedProperties); + + foreach (var prop in props) { + request.AddParameter(prop.Name, prop.Value, prop.Encode); + } + + return request; + } + + /// + /// Gets object properties and adds each property as a form data parameter + /// + /// + /// This method gets public instance properties from the provided type + /// rather than from itself, which allows for caching of properties and + /// other optimizations. If you don't know the type at runtime, or wish to use properties not + /// available from the provided type parameter, consider using + /// + /// Object to add as form data + /// Properties to include, or nothing to include everything. The array will be sorted. + /// + public RestRequest AddObjectStatic(T obj, params string[] includedProperties) where T : class + => request.AddParameters(PropertyCache.GetParameters(obj, includedProperties)); + + /// + /// Gets object properties and adds each property as a form data parameter + /// + /// + /// This method gets public instance properties from the provided type + /// rather than from itself, which allows for caching of properties and + /// other optimizations. If you don't know the type at runtime, or wish to use properties not + /// available from the provided type parameter, consider using + /// + /// Object to add as form data + /// + public RestRequest AddObjectStatic(T obj) where T : class + => request.AddParameters(PropertyCache.GetParameters(obj)); + } +} \ No newline at end of file diff --git a/src/RestSharp/Request/RestRequestExtensions.Query.cs b/src/RestSharp/Request/RestRequestExtensions.Query.cs new file mode 100644 index 000000000..265dc360d --- /dev/null +++ b/src/RestSharp/Request/RestRequestExtensions.Query.cs @@ -0,0 +1,42 @@ +// 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. + +namespace RestSharp; + +public static partial class RestRequestExtensions { + /// Request instance + extension(RestRequest request) { + /// + /// Adds a query string parameter to the request. The request resource should not contain any placeholders for this parameter. + /// The parameter will be added to the request URL as a query string using name=value format. + /// + /// Parameter name + /// Parameter value + /// Encode the value or not, default true + /// + public RestRequest AddQueryParameter(string name, string? value, bool encode = true) + => request.AddParameter(new QueryParameter(name, value, encode)); + + /// + /// Adds a query string parameter to the request. The request resource should not contain any placeholders for this parameter. + /// The parameter will be added to the request URL as a query string using name=value format. + /// + /// Parameter name + /// Parameter value + /// Encode the value or not, default true + /// + public RestRequest AddQueryParameter(string name, T value, bool encode = true) where T : struct + => request.AddQueryParameter(name, value.ToString(), encode); + } +} \ No newline at end of file diff --git a/src/RestSharp/Request/RestRequestExtensions.Url.cs b/src/RestSharp/Request/RestRequestExtensions.Url.cs new file mode 100644 index 000000000..499c629df --- /dev/null +++ b/src/RestSharp/Request/RestRequestExtensions.Url.cs @@ -0,0 +1,42 @@ +// 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. + +namespace RestSharp; + +public static partial class RestRequestExtensions { + /// Request instance + extension(RestRequest request) { + /// + /// Adds a URL segment parameter to the request. The resource URL must have a placeholder for the parameter for it to work. + /// For example, if you add a URL segment parameter with the name "id", the resource URL should contain {id} in its path. + /// + /// Name of the parameter; must be matching a placeholder in the resource URL as {name} + /// Value of the parameter + /// Encode the value or not, default true + /// + public RestRequest AddUrlSegment(string name, string? value, bool encode = true) + => request.AddOrUpdateParameter(new UrlSegmentParameter(name, value, encode)); + + /// + /// Adds a URL segment parameter to the request. The resource URL must have a placeholder for the parameter for it to work. + /// For example, if you add a URL segment parameter with the name "id", the resource URL should contain {id} in its path. + /// + /// Name of the parameter; must be matching a placeholder in the resource URL as {name} + /// Value of the parameter + /// Encode the value or not, default true + /// + public RestRequest AddUrlSegment(string name, T value, bool encode = true) where T : struct + => request.AddUrlSegment(name, value.ToString(), encode); + } +} \ No newline at end of file diff --git a/src/RestSharp/Request/RestRequestExtensions.cs b/src/RestSharp/Request/RestRequestExtensions.cs new file mode 100644 index 000000000..82323d07b --- /dev/null +++ b/src/RestSharp/Request/RestRequestExtensions.cs @@ -0,0 +1,141 @@ +// 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. + +namespace RestSharp; + +[PublicAPI] +public static partial class RestRequestExtensions { + /// + extension(RestRequest request) { + /// + /// Adds a HTTP parameter to the request (QueryString for GET, DELETE, OPTIONS and HEAD; Encoded form for POST and PUT) + /// + /// Name of the parameter + /// Value of the parameter + /// Encode the value or not, default true + /// This request + public RestRequest AddParameter(string name, string? value, bool encode = true) + => request.AddParameter(new GetOrPostParameter(name, value, encode)); + + /// + /// Adds a parameter of a given type to the request. It will create a typed parameter instance based on the type argument. + /// It is not recommended to use this overload unless you must, as it doesn't provide any restrictions, and if the name-value-type + /// combination doesn't match, it will throw. + /// + /// Name of the parameter, must be matching a placeholder in the resource URL as {name} + /// Value of the parameter + /// Enum value specifying what kind of parameter is being added + /// Encode the value or not, default true + /// + public RestRequest AddParameter(string? name, object value, ParameterType type, bool encode = true) + => type == ParameterType.RequestBody + ? request.AddBodyParameter(name, value) + : request.AddParameter(Parameter.CreateParameter(name, value, type, encode)); + + /// + /// Adds a HTTP parameter to the request (QueryString for GET, DELETE, OPTIONS and HEAD; Encoded form for POST and PUT). + /// The value will be converted to string. + /// + /// Name of the parameter + /// Value of the parameter + /// Encode the value or not, default true + /// This request + public RestRequest AddParameter(string name, T value, bool encode = true) where T : struct + => request.AddParameter(name, value.ToString(), encode); + + /// + /// Adds or updates a HTTP parameter to the request (QueryString for GET, DELETE, OPTIONS and HEAD; Encoded form for POST and PUT) + /// + /// Name of the parameter + /// Value of the parameter + /// Encode the value or not, default true + /// This request + public RestRequest AddOrUpdateParameter(string name, string? value, bool encode = true) + => request.AddOrUpdateParameter(new GetOrPostParameter(name, value, encode)); + + /// + /// Adds or updates a HTTP parameter to the request (QueryString for GET, DELETE, OPTIONS and HEAD; Encoded form for POST and PUT) + /// + /// Name of the parameter + /// Value of the parameter + /// Encode the value or not, default true + /// This request + public RestRequest AddOrUpdateParameter(string name, T value, bool encode = true) where T : struct + => request.AddOrUpdateParameter(name, value.ToString(), encode); + + RestRequest AddParameters(IEnumerable parameters) { + request.Parameters.AddParameters(parameters); + return request; + } + + /// + /// Adds or updates request parameter of a given type. It will create a typed parameter instance based on the type argument. + /// Parameter will be added or updated based on its name. If the request has a parameter with the same name, it will be updated. + /// It is not recommended to use this overload unless you must, as it doesn't provide any restrictions, and if the name-value-type + /// combination doesn't match, it will throw. + /// + /// Name of the parameter, must be matching a placeholder in the resource URL as {name} + /// Value of the parameter + /// Enum value specifying what kind of parameter is being added + /// Encode the value or not, default true + /// + public RestRequest AddOrUpdateParameter(string name, object value, ParameterType type, bool encode = true) { + request.RemoveParameter(name, type); + + return type == ParameterType.RequestBody + ? request.AddBodyParameter(name, value) + : request.AddOrUpdateParameter(Parameter.CreateParameter(name, value, type, encode)); + } + + /// + /// Adds or updates request parameter, given the parameter instance, for example or . + /// It will replace an existing parameter with the same name. + /// + /// Parameter instance + /// + public RestRequest AddOrUpdateParameter(Parameter parameter) + => request.RemoveParameter(parameter.Name, parameter.Type).AddParameter(parameter); + + /// + /// Adds or updates multiple request parameters, given the parameter instance, for example + /// or . Parameters with the same name will be replaced. + /// + /// Collection of parameter instances + /// + public RestRequest AddOrUpdateParameters(IEnumerable parameters) { + foreach (var parameter in parameters) request.AddOrUpdateParameter(parameter); + + return request; + } + + RestRequest RemoveParameter(string? name, ParameterType type) { + var p = request.Parameters.FirstOrDefault(x => x.Name == name && x.Type == type); + return p != null ? request.RemoveParameter(p) : request; + } + + /// + /// Adds cookie to the cookie container. + /// + /// Cookie name + /// Cookie value + /// Cookie path + /// Cookie domain, must not be an empty string + /// + public RestRequest AddCookie(string name, string value, string path, string domain) { + request.CookieContainer ??= new(); + request.CookieContainer.Add(new Cookie(name, value, path, domain)); + return request; + } + } +} \ No newline at end of file diff --git a/src/RestSharp/Request/RestXmlRequest.cs b/src/RestSharp/Request/RestXmlRequest.cs new file mode 100644 index 000000000..ada2eded2 --- /dev/null +++ b/src/RestSharp/Request/RestXmlRequest.cs @@ -0,0 +1,29 @@ +// 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. +// + +namespace RestSharp; + +public class RestXmlRequest : RestRequest { + /// + /// Used by the default deserializers to explicitly set which date format string to use when parsing dates. + /// + public string? DateFormat { get; set; } + + /// + /// Used by XmlDeserializer. If not specified, XmlDeserializer will flatten response by removing namespaces from + /// element names. + /// + public string? XmlNamespace { get; set; } +} \ No newline at end of file diff --git a/src/RestSharp/Request/UriExtensions.cs b/src/RestSharp/Request/UriExtensions.cs new file mode 100644 index 000000000..9cb26ebe8 --- /dev/null +++ b/src/RestSharp/Request/UriExtensions.cs @@ -0,0 +1,83 @@ +// 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. +// + +using RestSharp.Extensions; + +namespace RestSharp; + +static class UriExtensions { + public static Uri MergeBaseUrlAndResource(this Uri? baseUrl, string? resource) { + var assembled = resource; + + if (assembled.IsNotEmpty() && assembled[0] == '/') assembled = assembled[1..]; + + if (baseUrl == null || baseUrl.AbsoluteUri.IsEmpty()) { + return assembled.IsNotEmpty() + ? new Uri(assembled) + : throw new ArgumentException("Both BaseUrl and Resource are empty", nameof(resource)); + } + + var usingBaseUri = baseUrl.AbsoluteUri[^1] == '/' || assembled.IsEmpty() ? baseUrl : new(baseUrl.AbsoluteUri + "/"); + +#if NETSTANDARD2_0 + return !string.IsNullOrWhiteSpace(assembled) ? new(usingBaseUri, assembled, true) : baseUrl; +#else + return !string.IsNullOrWhiteSpace(assembled) ? new(usingBaseUri, assembled) : baseUrl; +#endif + } + + public static Uri AddQueryString(this Uri uri, string? query) { + if (query == null) return uri; + + var absoluteUri = uri.AbsoluteUri; + var separator = absoluteUri.Contains('?') ? "&" : "?"; + + return new($"{absoluteUri}{separator}{query}"); + } + + public static UrlSegmentParamsValues GetUrlSegmentParamsValues( + this Uri? baseUri, + string resource, + Func encode, + params ParametersCollection[] parametersCollections + ) { + var assembled = baseUri == null ? "" : resource; +#if NETSTANDARD2_0 + var baseUrl = baseUri ?? new Uri(resource, true); +#else + var baseUrl = baseUri ?? new Uri(resource); +#endif + + var hasResource = !assembled.IsEmpty(); + + var parameters = parametersCollections.SelectMany(x => x.GetParameters()); + + var builder = new UriBuilder(baseUrl); + + foreach (var parameter in parameters) { + var paramPlaceHolder = $"{{{parameter.Name}}}"; + var value = Ensure.NotNull(parameter.Value!.ToString(), $"URL segment parameter {parameter.Name} value"); + var paramValue = parameter.Encode ? encode(value) : value; + + if (hasResource) assembled = assembled.Replace(paramPlaceHolder, paramValue); + + builder.Path = builder.Path.UrlDecode().Replace(paramPlaceHolder, paramValue); + } + + return new(builder.Uri, assembled); + } +} + +record UrlSegmentParamsValues(Uri Uri, string Resource); \ No newline at end of file diff --git a/src/RestSharp/Response/ResponseThrowExtension.cs b/src/RestSharp/Response/ResponseThrowExtension.cs new file mode 100644 index 000000000..01d5167af --- /dev/null +++ b/src/RestSharp/Response/ResponseThrowExtension.cs @@ -0,0 +1,32 @@ +// 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. +// + +namespace RestSharp; + +public static class ResponseThrowExtension { + extension(RestResponse response) { + public RestResponse ThrowIfError() { + var exception = response.GetException(); + return exception != null ? throw exception : response; + } + } + + extension(RestResponse response) { + public RestResponse ThrowIfError() { + var exception = response.GetException(); + return exception != null ? throw exception : response; + } + } +} diff --git a/src/RestSharp/Response/RestResponse.cs b/src/RestSharp/Response/RestResponse.cs new file mode 100644 index 000000000..ef08e6bd3 --- /dev/null +++ b/src/RestSharp/Response/RestResponse.cs @@ -0,0 +1,84 @@ +// 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. + +using System.Diagnostics; +using RestSharp.Extensions; + +// ReSharper disable SuggestBaseTypeForParameter + +namespace RestSharp; + +/// +/// Container for data sent back from API including deserialized data +/// +/// Type of data to deserialize to +[GenerateClone(BaseType = typeof(RestResponse), Name = "FromResponse")] +[DebuggerDisplay($"{{{nameof(DebuggerDisplay)}()}}")] +public partial class RestResponse(RestRequest request) : RestResponse(request) { + /// + /// Deserialized entity data + /// + public T? Data { get; set; } +} + +/// +/// Container for data sent back from API +/// +[DebuggerDisplay($"{{{nameof(DebuggerDisplay)}()}}")] +public class RestResponse(RestRequest request) : RestResponseBase(request) { + internal static async Task FromHttpResponse( + HttpResponseMessage httpResponse, + RestRequest request, + ReadOnlyRestClientOptions options, + CookieCollection? cookieCollection, + CancellationToken cancellationToken + ) { + return request.AdvancedResponseWriter?.Invoke(httpResponse, request) ?? await GetDefaultResponse().ConfigureAwait(false); + + async Task GetDefaultResponse() { +#if NET + await using var stream = await httpResponse.ReadResponseStream(request.ResponseWriter, cancellationToken).ConfigureAwait(false); +#else + using var stream = await httpResponse.ReadResponseStream(request.ResponseWriter, cancellationToken).ConfigureAwait(false); +#endif + + var bytes = stream == null ? null : await stream.ReadAsBytes(cancellationToken).ConfigureAwait(false); + var content = bytes == null ? null : await httpResponse.GetResponseString(bytes, options.Encoding); + + return new(request) { + Content = content, + ContentEncoding = httpResponse.Content?.Headers.ContentEncoding ?? Array.Empty(), + ContentHeaders = httpResponse.Content?.Headers.GetHeaderParameters(), + ContentLength = httpResponse.Content?.Headers.ContentLength, + ContentType = httpResponse.Content?.Headers.ContentType?.MediaType, + Cookies = cookieCollection, + ErrorException = httpResponse.MaybeException(options.SetErrorExceptionOnUnsuccessfulStatusCode), + Headers = httpResponse.Headers.GetHeaderParameters(), + IsSuccessStatusCode = httpResponse.IsSuccessStatusCode, + RawBytes = bytes, + ResponseStatus = options.CalculateResponseStatus(httpResponse), + ResponseUri = httpResponse.RequestMessage?.RequestUri, + RootElement = request.RootElement, + Server = httpResponse.Headers.Server.ToString(), + StatusCode = httpResponse.StatusCode, + StatusDescription = httpResponse.ReasonPhrase, + Version = httpResponse.RequestMessage?.Version + }; + } + } + + public RestResponse() : this(new()) { } +} + +public delegate ResponseStatus CalculateResponseStatus(HttpResponseMessage httpResponse); \ No newline at end of file diff --git a/src/RestSharp/Response/RestResponseBase.cs b/src/RestSharp/Response/RestResponseBase.cs new file mode 100644 index 000000000..383895fb4 --- /dev/null +++ b/src/RestSharp/Response/RestResponseBase.cs @@ -0,0 +1,160 @@ +// 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. + +using System.Diagnostics; +// ReSharper disable PropertyCanBeMadeInitOnly.Global +// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global + +namespace RestSharp; + +/// +/// Base class for common properties shared by RestResponse and RestResponse[[T]] +/// +[DebuggerDisplay($"{{{nameof(DebuggerDisplay)}()}}")] +public abstract class RestResponseBase { + /// + /// Default constructor + /// + protected RestResponseBase(RestRequest request) { + ResponseStatus = ResponseStatus.None; + Request = request; + Request.IncreaseNumberOfAttempts(); + } + + /// + /// The RestRequest that was made to get this RestResponse + /// + /// + /// Mainly for debugging if ResponseStatus is not OK + /// + public RestRequest Request { get; set; } + + /// + /// MIME content type of response + /// + public string? ContentType { get; set; } + + /// + /// Length in bytes of the response content + /// + public long? ContentLength { get; set; } + + /// + /// Encoding of the response content + /// + public ICollection ContentEncoding { get; set; } = Array.Empty(); + + /// + /// String representation of response content + /// + public string? Content { get; set; } + + /// + /// HTTP response status code + /// + public HttpStatusCode StatusCode { get; set; } + + /// + /// Whether the HTTP response status code indicates success + /// + public bool IsSuccessStatusCode { get; set; } + + /// + /// Whether the HTTP response status code indicates success and no other error occurred + /// (deserialization, timeout, ...) + /// + public bool IsSuccessful => IsSuccessStatusCode && ResponseStatus == ResponseStatus.Completed; + + /// + /// Description of HTTP status returned + /// + public string? StatusDescription { get; set; } + + /// + /// Response content + /// + public byte[]? RawBytes { get; set; } + + /// + /// The URL that actually responded to the content (different from request if redirected) + /// + public Uri? ResponseUri { get; set; } + + /// + /// Server header value + /// + public string? Server { get; set; } + + /// + /// Cookies returned by server with the response + /// + public CookieCollection? Cookies { get; set; } + + /// + /// Response headers returned by server with the response + /// + public IReadOnlyCollection? Headers { get; set; } + + /// + /// Content headers returned by server with the response + /// + public IReadOnlyCollection? ContentHeaders { get; set; } + + /// + /// Status of the request. Will return Error for transport errors. + /// HTTP errors will still return ResponseStatus.Completed, check StatusCode instead + /// + public ResponseStatus ResponseStatus { get; set; } + + /// + /// Transport or another non-HTTP error generated while attempting request + /// + public string? ErrorMessage { get; set; } + + /// + /// The exception thrown during the request, if any + /// + public Exception? ErrorException { get; set; } + + /// + /// HTTP protocol version of the request + /// + public Version? Version { get; set; } + + /// + /// Root element of the serialized response content, only works if deserializer supports it + /// + public string? RootElement { get; set; } + + /// + /// Assists with debugging responses by displaying in the debugger output + /// + /// + protected string DebuggerDisplay() => $"StatusCode: {StatusCode}, Content-Type: {ContentType}, Content-Length: {ContentLength})"; + + internal Exception? GetException() + => ResponseStatus switch { + ResponseStatus.Aborted => new HttpRequestException("Request aborted", ErrorException), + ResponseStatus.Error => ErrorException, + ResponseStatus.TimedOut => new TimeoutException("Request timed out", ErrorException), + ResponseStatus.None => null, + ResponseStatus.Completed => null, + _ => throw ErrorException ?? new ArgumentOutOfRangeException(nameof(ResponseStatus)) + }; + + internal void AddException(Exception exception) { + ErrorException = exception; + ErrorMessage = exception.Message; + } +} diff --git a/src/RestSharp/Response/RestResponseExtensions.cs b/src/RestSharp/Response/RestResponseExtensions.cs new file mode 100644 index 000000000..ed9b32721 --- /dev/null +++ b/src/RestSharp/Response/RestResponseExtensions.cs @@ -0,0 +1,63 @@ +// 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. + +namespace RestSharp; + +public static class RestResponseExtensions { + /// Response object + extension(RestResponse response) { + /// + /// Gets the value of the header with the specified name. + /// + /// Name of the header + /// Header value or null if the header is not found in the response + public string? GetHeaderValue(string headerName) + => response.Headers?.FirstOrDefault(x => NameIs(x.Name, headerName))?.Value.ToString(); + + /// + /// Gets all the values of the header with the specified name. + /// + /// Name of the header + /// Array of header values or empty array if the header is not found in the response + public string[] GetHeaderValues(string headerName) + => response.Headers + ?.Where(x => NameIs(x.Name, headerName)) + .Select(x => x.Value.ToString() ?? "") + .ToArray() ?? + []; + + /// + /// Gets the value of the content header with the specified name. + /// + /// Name of the header + /// Header value or null if the content header is not found in the response + public string? GetContentHeaderValue(string headerName) + => response.ContentHeaders?.FirstOrDefault(x => NameIs(x.Name, headerName))?.Value.ToString(); + + /// + /// Gets all the values of the content header with the specified name. + /// + /// Name of the header + /// Array of header values or empty array if the content header is not found in the response + public string[] GetContentHeaderValues(string headerName) + => response.ContentHeaders + ?.Where(x => NameIs(x.Name, headerName)) + .Select(x => x.Value.ToString() ?? "") + .ToArray() ?? + []; + } + + static bool NameIs(string? name, string headerName) + => name != null && name.Equals(headerName, StringComparison.InvariantCultureIgnoreCase); +} \ No newline at end of file diff --git a/src/RestSharp/RestClient.Async.cs b/src/RestSharp/RestClient.Async.cs new file mode 100644 index 000000000..75c8ac85f --- /dev/null +++ b/src/RestSharp/RestClient.Async.cs @@ -0,0 +1,226 @@ +// 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. + +using RestSharp.Extensions; + +// ReSharper disable PossiblyMistakenUseOfCancellationToken + +namespace RestSharp; + +public partial class RestClient { + // Default HttpClient timeout + readonly TimeSpan _defaultTimeout = TimeSpan.FromSeconds(100); + + /// + public async Task ExecuteAsync(RestRequest request, CancellationToken cancellationToken = default) { + using var internalResponse = await ExecuteRequestAsync(request, cancellationToken).ConfigureAwait(false); + + var response = internalResponse.Exception == null + ? await RestResponse.FromHttpResponse( + internalResponse.ResponseMessage!, + request, + Options, + internalResponse.CookieContainer?.GetCookies(internalResponse.Url), + cancellationToken + ) + .ConfigureAwait(false) + : GetErrorResponse(request, internalResponse.Exception, internalResponse.TimeoutToken); + await OnAfterRequest(response, cancellationToken).ConfigureAwait(false); + + return Options.ThrowOnAnyError ? response.ThrowIfError() : response; + } + + /// + [PublicAPI] + public async Task DownloadStreamAsync(RestRequest request, CancellationToken cancellationToken = default) { + // Make sure we only read the headers, so we can stream the content body efficiently + request.CompletionOption = HttpCompletionOption.ResponseHeadersRead; + var response = await ExecuteRequestAsync(request, cancellationToken).ConfigureAwait(false); + + var exception = response.Exception ?? response.ResponseMessage?.MaybeException(Options.SetErrorExceptionOnUnsuccessfulStatusCode); + + if (exception != null) { + return Options.ThrowOnAnyError ? throw exception : null; + } + + if (response.ResponseMessage == null) return null; + + return await response.ResponseMessage.ReadResponseStream(request.ResponseWriter, cancellationToken).ConfigureAwait(false); + } + + static RestResponse GetErrorResponse(RestRequest request, Exception exception, CancellationToken timeoutToken) { + var response = new RestResponse(request) { + ResponseStatus = exception is OperationCanceledException + ? TimedOut() ? ResponseStatus.TimedOut : ResponseStatus.Aborted + : ResponseStatus.Error, + ErrorMessage = exception.Message, + ErrorException = exception + }; + + return response; + + bool TimedOut() => timeoutToken.IsCancellationRequested || exception.Message.Contains("HttpClient.Timeout"); + } + + void CombineInterceptors(RestRequest request) { + if (request.Interceptors == null) { + if (Options.Interceptors == null) { + return; + } + + request.Interceptors = Options.Interceptors.ToList(); + return; + } + + if (Options.Interceptors != null) { + request.Interceptors.AddRange(Options.Interceptors); + } + } + + async Task ExecuteRequestAsync(RestRequest request, CancellationToken cancellationToken) { + Ensure.NotNull(request, nameof(request)); + + // Make sure we are not disposed of when someone tries to call us! +#if NET + ObjectDisposedException.ThrowIf(_disposed, this); +#else + if (_disposed) { + throw new ObjectDisposedException(nameof(RestClient)); + } +#endif + CombineInterceptors(request); + await OnBeforeRequest(request, cancellationToken).ConfigureAwait(false); + request.ValidateParameters(); + var authenticator = request.Authenticator ?? Options.Authenticator; + + if (authenticator != null) { + await authenticator.Authenticate(this, request).ConfigureAwait(false); + } + + using var requestContent = new RequestContent(this, request); + + var httpMethod = AsHttpMethod(request.Method); + var url = this.BuildUri(request); + + using var message = new HttpRequestMessage(httpMethod, url); + message.Content = requestContent.BuildContent(); + message.Headers.Host = Options.BaseHost; + message.Headers.CacheControl = request.CachePolicy ?? Options.CachePolicy; + message.Version = request.Version; + + using var timeoutCts = new CancellationTokenSource(request.Timeout ?? Options.Timeout ?? _defaultTimeout); + using var cts = CancellationTokenSource.CreateLinkedTokenSource(timeoutCts.Token, cancellationToken); + + var ct = cts.Token; + + HttpResponseMessage? responseMessage; + // Make sure we have a cookie container if not provided in the request + var cookieContainer = request.CookieContainer ??= new(); + + var headers = new RequestHeaders() + .AddHeaders(request.Parameters) + .AddHeaders(DefaultParameters) + .AddAcceptHeader(AcceptedContentTypes) + .AddCookieHeaders(url, cookieContainer) + .AddCookieHeaders(url, Options.CookieContainer); + + message.AddHeaders(headers); +#pragma warning disable CS0618 // Type or member is obsolete + if (request.OnBeforeRequest != null) await request.OnBeforeRequest(message).ConfigureAwait(false); +#pragma warning restore CS0618 // Type or member is obsolete + await OnBeforeHttpRequest(request, message, cancellationToken).ConfigureAwait(false); + + try { + responseMessage = await HttpClient.SendAsync(message, request.CompletionOption, ct).ConfigureAwait(false); + + // Parse all the cookies from the response and update the cookie jar with cookies + if (responseMessage.Headers.TryGetValues(KnownHeaders.SetCookie, out var cookiesHeader)) { + // ReSharper disable once PossibleMultipleEnumeration + cookieContainer.AddCookies(url, cookiesHeader); + // ReSharper disable once PossibleMultipleEnumeration + Options.CookieContainer?.AddCookies(url, cookiesHeader); + } + } + catch (Exception ex) { + return new(null, url, null, ex, timeoutCts.Token); + } + +#pragma warning disable CS0618 // Type or member is obsolete + if (request.OnAfterRequest != null) await request.OnAfterRequest(responseMessage).ConfigureAwait(false); +#pragma warning restore CS0618 // Type or member is obsolete + await OnAfterHttpRequest(request, responseMessage, cancellationToken).ConfigureAwait(false); + return new(responseMessage, url, cookieContainer, null, timeoutCts.Token); + } + + static async ValueTask OnBeforeRequest(RestRequest request, CancellationToken cancellationToken) { + if (request.Interceptors == null) return; + + foreach (var interceptor in request.Interceptors) { + await interceptor.BeforeRequest(request, cancellationToken).ConfigureAwait(false); + } + } + + static async ValueTask OnBeforeHttpRequest(RestRequest request, HttpRequestMessage requestMessage, CancellationToken cancellationToken) { + if (request.Interceptors == null) return; + + foreach (var interceptor in request.Interceptors) { + await interceptor.BeforeHttpRequest(requestMessage, cancellationToken).ConfigureAwait(false); + } + } + + static async ValueTask OnAfterHttpRequest(RestRequest request, HttpResponseMessage responseMessage, CancellationToken cancellationToken) { + if (request.Interceptors == null) return; + + foreach (var interceptor in request.Interceptors) { + await interceptor.AfterHttpRequest(responseMessage, cancellationToken).ConfigureAwait(false); + } + } + + static async ValueTask OnAfterRequest(RestResponse response, CancellationToken cancellationToken) { + if (response.Request.Interceptors == null) return; + + foreach (var interceptor in response.Request.Interceptors) { + await interceptor.AfterRequest(response, cancellationToken).ConfigureAwait(false); + } + } + + record HttpResponse( + HttpResponseMessage? ResponseMessage, + Uri Url, + CookieContainer? CookieContainer, + Exception? Exception, + CancellationToken TimeoutToken + ) : IDisposable { + public void Dispose() => ResponseMessage?.Dispose(); + } + + internal static HttpMethod AsHttpMethod(Method method) + => method switch { + Method.Get => HttpMethod.Get, + Method.Post => HttpMethod.Post, + Method.Put => HttpMethod.Put, + Method.Delete => HttpMethod.Delete, + Method.Head => HttpMethod.Head, + Method.Options => HttpMethod.Options, +#if NET + Method.Patch => HttpMethod.Patch, +#else + Method.Patch => new("PATCH"), +#endif + Method.Merge => new("MERGE"), + Method.Copy => new("COPY"), + Method.Search => new("SEARCH"), + _ => throw new ArgumentOutOfRangeException(nameof(method)) + }; +} \ No newline at end of file diff --git a/src/RestSharp/RestClient.Extensions.Delete.cs b/src/RestSharp/RestClient.Extensions.Delete.cs new file mode 100644 index 000000000..c92636545 --- /dev/null +++ b/src/RestSharp/RestClient.Extensions.Delete.cs @@ -0,0 +1,179 @@ +// 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. +// + +namespace RestSharp; + +public static partial class RestClientExtensions { + /// + extension(IRestClient client) { + /// + /// Executes a DELETE-style request asynchronously, authenticating if needed + /// + /// Request to be executed + /// Cancellation token + public Task ExecuteDeleteAsync(RestRequest request, CancellationToken cancellationToken = default) + => client.ExecuteAsync(request, Method.Delete, cancellationToken); + + /// + /// Executes a DELETE-style request asynchronously, authenticating if needed + /// + /// Request resource + /// Cancellation token + public Task ExecuteDeleteAsync(string resource, CancellationToken cancellationToken = default) + => client.ExecuteAsync(new(resource), Method.Delete, cancellationToken); + + /// + /// Executes a DELETE-style synchronously, authenticating if needed + /// + /// Request resource + public RestResponse ExecuteDelete(string resource) + => AsyncHelpers.RunSync(() => client.ExecuteDeleteAsync(resource)); + + /// + /// Executes a DELETE-style synchronously, authenticating if needed + /// + /// Request to be executed + public RestResponse ExecuteDelete(RestRequest request) + => AsyncHelpers.RunSync(() => client.ExecuteAsync(request, Method.Delete)); + + /// + /// Executes a DELETE-style request asynchronously, authenticating if needed. + /// The response content then gets deserialized to T. + /// + /// Target deserialization type + /// Request to be executed + /// The cancellation token + /// Deserialized response content + public Task> ExecuteDeleteAsync( + RestRequest request, + CancellationToken cancellationToken = default + ) + => client.ExecuteAsync(request, Method.Delete, cancellationToken); + + /// + /// Executes a DELETE-style request asynchronously, authenticating if needed. + /// The response content then gets deserialized to T. + /// + /// Target deserialization type + /// Request resource + /// The cancellation token + /// Deserialized response content + public Task> ExecuteDeleteAsync( + string resource, + CancellationToken cancellationToken = default + ) + => client.ExecuteAsync(new(resource), Method.Delete, cancellationToken); + + /// + /// Executes a DELETE-style request synchronously, authenticating if needed. + /// The response content then gets deserialized to T. + /// + /// Target deserialization type + /// Request to be executed + /// Deserialized response content + public RestResponse ExecuteDelete(RestRequest request) + => AsyncHelpers.RunSync(() => client.ExecuteAsync(request, Method.Delete)); + + /// + /// Executes a DELETE-style request synchronously, authenticating if needed. + /// The response content then gets deserialized to T. + /// + /// Target deserialization type + /// Request resource + /// Deserialized response content + public RestResponse ExecuteDelete(string resource) + => AsyncHelpers.RunSync(() => client.ExecuteDeleteAsync(resource)); + + /// + /// Execute the request using DELETE HTTP method. Exception will be thrown if the request does not succeed. + /// The response data is deserialized to the Data property of the returned response object. + /// + /// The request + /// Cancellation token + /// Expected result type + /// + public async Task DeleteAsync(RestRequest request, CancellationToken cancellationToken = default) { + var response = await client.ExecuteAsync(request, Method.Delete, cancellationToken).ConfigureAwait(false); + return response.ThrowIfError().Data; + } + + /// + /// Execute the request using DELETE HTTP method. Exception will be thrown if the request does not succeed. + /// The response data is deserialized to the Data property of the returned response object. + /// + /// Request resource + /// Cancellation token + /// Expected result type + /// + public async Task DeleteAsync(string resource, CancellationToken cancellationToken = default) { + var response = await client.ExecuteAsync(new(resource), Method.Delete, cancellationToken).ConfigureAwait(false); + return response.ThrowIfError().Data; + } + + /// + /// Execute the request using DELETE HTTP method. Exception will be thrown if the request does not succeed. + /// The response data is deserialized to the Data property of the returned response object. + /// + /// The request + /// Expected result type + /// + public T? Delete(RestRequest request) => AsyncHelpers.RunSync(() => client.DeleteAsync(request)); + + /// + /// Execute the request using DELETE HTTP method. Exception will be thrown if the request does not succeed. + /// The response data is deserialized to the Data property of the returned response object. + /// + /// Request resource + /// Expected result type + /// + public T? Delete(string resource) => AsyncHelpers.RunSync(() => client.DeleteAsync(resource)); + + /// + /// Execute the request using DELETE HTTP method. Exception will be thrown if the request does not succeed. + /// + /// Request resource + /// Cancellation token + /// + public async Task DeleteAsync(string resource, CancellationToken cancellationToken = default) { + var response = await client.ExecuteAsync(new(resource), Method.Delete, cancellationToken).ConfigureAwait(false); + return response.ThrowIfError(); + } + + /// + /// Execute the request using DELETE HTTP method. Exception will be thrown if the request does not succeed. + /// + /// Request resource + /// + public RestResponse Delete(string resource) => AsyncHelpers.RunSync(() => client.DeleteAsync(resource)); + + /// + /// Execute the request using DELETE HTTP method. Exception will be thrown if the request does not succeed. + /// + /// The request + /// Cancellation token + /// + public async Task DeleteAsync(RestRequest request, CancellationToken cancellationToken = default) { + var response = await client.ExecuteAsync(request, Method.Delete, cancellationToken).ConfigureAwait(false); + return response.ThrowIfError(); + } + + /// + /// Execute the request using DELETE HTTP method. Exception will be thrown if the request does not succeed. + /// + /// The request + /// + public RestResponse Delete(RestRequest request) => AsyncHelpers.RunSync(() => client.DeleteAsync(request)); + } +} \ No newline at end of file diff --git a/src/RestSharp/RestClient.Extensions.Get.cs b/src/RestSharp/RestClient.Extensions.Get.cs new file mode 100644 index 000000000..1dae80183 --- /dev/null +++ b/src/RestSharp/RestClient.Extensions.Get.cs @@ -0,0 +1,210 @@ +// 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. +// + +namespace RestSharp; + +public static partial class RestClientExtensions { + /// + extension(IRestClient client) { + /// + /// Executes a GET-style asynchronously, authenticating if needed. + /// + /// Request to be executed + /// Cancellation token + public Task ExecuteGetAsync(RestRequest request, CancellationToken cancellationToken = default) + => client.ExecuteAsync(request, Method.Get, cancellationToken); + + /// + /// Executes a GET-style asynchronously, authenticating if needed. + /// + /// Request resource + /// Cancellation token + public Task ExecuteGetAsync(string resource, CancellationToken cancellationToken = default) + => client.ExecuteAsync(new(resource), Method.Get, cancellationToken); + + /// + /// Executes a GET-style synchronously, authenticating if needed + /// + /// Request to be executed + public RestResponse ExecuteGet(RestRequest request) + => AsyncHelpers.RunSync(() => client.ExecuteAsync(request, Method.Get)); + + /// + /// Executes a GET-style request asynchronously, authenticating if needed. + /// The response content then gets deserialized to T. + /// + /// Target deserialization type + /// Request to be executed + /// Cancellation token + /// Deserialized response content + public Task> ExecuteGetAsync( + RestRequest request, + CancellationToken cancellationToken = default + ) + => client.ExecuteAsync(request, Method.Get, cancellationToken); + + /// + /// Executes a GET-style request to the specified resource URL asynchronously, authenticating if needed. + /// The response content then gets deserialized to T. + /// + /// Target deserialization type + /// Request resource + /// Cancellation token + /// Deserialized response content + public Task> ExecuteGetAsync( + string resource, + CancellationToken cancellationToken = default + ) + => client.ExecuteAsync(new(resource), Method.Get, cancellationToken); + + /// + /// Executes a GET-style request synchronously, authenticating if needed. + /// The response content then gets deserialized to T. + /// + /// Target deserialization type + /// Request to be executed + /// Deserialized response content + public RestResponse ExecuteGet(RestRequest request) + => AsyncHelpers.RunSync(() => client.ExecuteAsync(request, Method.Get)); + + /// + /// Executes a GET-style request synchronously, authenticating if needed. + /// The response content then gets deserialized to T. + /// + /// Target deserialization type + /// Request resource + /// Deserialized response content + public RestResponse ExecuteGet(string resource) + => AsyncHelpers.RunSync(() => client.ExecuteGetAsync(resource)); + + /// + /// Execute the request using GET HTTP method. Exception will be thrown if the request does not succeed. + /// + /// The request + /// Cancellation token + /// + public async Task GetAsync(RestRequest request, CancellationToken cancellationToken = default) { + var response = await client.ExecuteGetAsync(request, cancellationToken).ConfigureAwait(false); + return response.ThrowIfError(); + } + + /// + /// Execute the request using GET HTTP method. Exception will be thrown if the request does not succeed. + /// + /// The request + /// + public RestResponse Get(RestRequest request) => AsyncHelpers.RunSync(() => client.GetAsync(request)); + + /// + /// Execute the request using GET HTTP method. Exception will be thrown if the request does not succeed. + /// The response data is deserialized to the Data property of the returned response object. + /// + /// The request + /// Cancellation token + /// Expected result type + /// + public async Task GetAsync(RestRequest request, CancellationToken cancellationToken = default) { + var response = await client.ExecuteGetAsync(request, cancellationToken).ConfigureAwait(false); + return response.ThrowIfError().Data; + } + + /// + /// Execute the request using GET HTTP method. Exception will be thrown if the request does not succeed. + /// The response data is deserialized to the Data property of the returned response object. + /// + /// The request + /// Expected result type + /// + public T? Get(RestRequest request) => AsyncHelpers.RunSync(() => client.GetAsync(request)); + + /// + /// Calls the URL specified in the resource parameter, expecting a JSON response back. Deserializes and returns the response. + /// + /// Resource URL + /// Cancellation token + /// Response object type + /// + public Task GetAsync(string resource, CancellationToken cancellationToken = default) + => client.GetAsync(new RestRequest(resource), cancellationToken); + + [Obsolete("Use GetAsync instead")] + public Task GetJsonAsync( + string resource, + CancellationToken cancellationToken = default + ) + => client.GetAsync(resource, cancellationToken); + + [Obsolete("Use Get instead")] + public TResponse? GetJson(string resource) + => AsyncHelpers.RunSync(() => client.GetAsync(resource)); + + /// + /// Calls the URL specified in the resource parameter, expecting a JSON response back. Deserializes and returns the response. + /// + /// Resource URL + /// Response object type + /// Deserialized response object + public TResponse? Get(string resource) + => AsyncHelpers.RunSync(() => client.GetAsync(resource)); + + /// + /// Calls the URL specified in the resource parameter, expecting a JSON response back. Deserializes and returns the response. + /// + /// Resource URL + /// Parameters to pass to the request + /// Cancellation token + /// Response object type + /// Deserialized response object + public Task GetAsync( + string resource, + object parameters, + CancellationToken cancellationToken = default + ) { + var props = parameters.GetProperties(); + var request = new RestRequest(resource); + + foreach (var prop in props) { + Parameter parameter = resource.Contains($"{prop.Name}") + ? new UrlSegmentParameter(prop.Name, prop.Value!, prop.Encode) + : new QueryParameter(prop.Name, prop.Value, prop.Encode); + request.AddParameter(parameter); + } + + return client.GetAsync(request, cancellationToken); + } + + [Obsolete("Use GetAsync instead")] + public Task GetJsonAsync( + string resource, + object parameters, + CancellationToken cancellationToken = default + ) + => client.GetAsync(resource, parameters, cancellationToken); + + /// + /// Calls the URL specified in the resource parameter, expecting a JSON response back. Deserializes and returns the response. + /// + /// Resource URL + /// Parameters to pass to the request + /// Response object type + /// Deserialized response object + public TResponse? Get(string resource, object parameters) + => AsyncHelpers.RunSync(() => client.GetAsync(resource, parameters)); + + [Obsolete("Use Get instead")] + public TResponse? GetJson(string resource, object parameters) + => client.Get(resource, parameters); + } +} \ No newline at end of file diff --git a/src/RestSharp/RestClient.Extensions.Head.cs b/src/RestSharp/RestClient.Extensions.Head.cs new file mode 100644 index 000000000..ee4a9421a --- /dev/null +++ b/src/RestSharp/RestClient.Extensions.Head.cs @@ -0,0 +1,100 @@ +// 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. +// + +namespace RestSharp; + +public static partial class RestClientExtensions { + /// + extension(IRestClient client) { + /// + /// Executes a HEAD-style request asynchronously, authenticating if needed + /// + /// Request to be executed + /// Cancellation token + public Task ExecuteHeadAsync(RestRequest request, CancellationToken cancellationToken = default) + => client.ExecuteAsync(request, Method.Head, cancellationToken); + + /// + /// Executes a HEAD-style synchronously, authenticating if needed + /// + /// Request to be executed + public RestResponse ExecuteHead(RestRequest request) + => AsyncHelpers.RunSync(() => client.ExecuteAsync(request, Method.Head)); + + /// + /// Executes a HEAD-style request asynchronously, authenticating if needed. + /// The response content then gets deserialized to T. + /// + /// Target deserialization type + /// Request to be executed + /// The cancellation token + /// Deserialized response content + public Task> ExecuteHeadAsync( + RestRequest request, + CancellationToken cancellationToken = default + ) + => client.ExecuteAsync(request, Method.Head, cancellationToken); + + /// + /// Executes a HEAD-style request synchronously, authenticating if needed. + /// The response content then gets deserialized to T. + /// + /// Target deserialization type + /// Request to be executed + /// Deserialized response content + public RestResponse ExecuteHead(RestRequest request) + => AsyncHelpers.RunSync(() => client.ExecuteAsync(request, Method.Head)); + + /// + /// Execute the request using HEAD HTTP method. Exception will be thrown if the request does not succeed. + /// The response data is deserialized to the Data property of the returned response object. + /// + /// The request + /// Cancellation token + /// Expected result type + /// + public async Task HeadAsync(RestRequest request, CancellationToken cancellationToken = default) { + var response = await client.ExecuteAsync(request, Method.Head, cancellationToken).ConfigureAwait(false); + return response.ThrowIfError().Data; + } + + /// + /// Execute the request using HEAD HTTP method. Exception will be thrown if the request does not succeed. + /// The response data is deserialized to the Data property of the returned response object. + /// + /// The request + /// Expected result type + /// + public T? Head(RestRequest request) => AsyncHelpers.RunSync(() => client.HeadAsync(request)); + + /// + /// Execute the request using HEAD HTTP method. Exception will be thrown if the request does not succeed. + /// + /// The request + /// Cancellation token + /// + public async Task HeadAsync(RestRequest request, CancellationToken cancellationToken = default) { + var response = await client.ExecuteAsync(request, Method.Head, cancellationToken).ConfigureAwait(false); + return response.ThrowIfError(); + } + + /// + /// Execute the request using HEAD HTTP method. Exception will be thrown if the request does not succeed. + /// + /// The request + /// + public RestResponse Head(RestRequest request) => AsyncHelpers.RunSync(() => client.HeadAsync(request)); + } +} diff --git a/src/RestSharp/RestClient.Extensions.Options.cs b/src/RestSharp/RestClient.Extensions.Options.cs new file mode 100644 index 000000000..82882394a --- /dev/null +++ b/src/RestSharp/RestClient.Extensions.Options.cs @@ -0,0 +1,100 @@ +// 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. +// + +namespace RestSharp; + +public static partial class RestClientExtensions { + /// + extension(IRestClient client) { + /// + /// Executes an OPTIONS-style request asynchronously, authenticating if needed + /// + /// Request to be executed + /// Cancellation token + public Task ExecuteOptionsAsync(RestRequest request, CancellationToken cancellationToken = default) + => client.ExecuteAsync(request, Method.Options, cancellationToken); + + /// + /// Executes a OPTIONS-style synchronously, authenticating if needed + /// + /// Request to be executed + public RestResponse ExecuteOptions(RestRequest request) + => AsyncHelpers.RunSync(() => client.ExecuteAsync(request, Method.Options)); + + /// + /// Executes a OPTIONS-style request asynchronously, authenticating if needed. + /// The response content then gets deserialized to T. + /// + /// Target deserialization type + /// Request to be executed + /// The cancellation token + /// Deserialized response content + public Task> ExecuteOptionsAsync( + RestRequest request, + CancellationToken cancellationToken = default + ) + => client.ExecuteAsync(request, Method.Options, cancellationToken); + + /// + /// Executes a OPTIONS-style request synchronously, authenticating if needed. + /// The response content then gets deserialized to T. + /// + /// Target deserialization type + /// Request to be executed + /// Deserialized response content + public RestResponse ExecuteOptions(RestRequest request) + => AsyncHelpers.RunSync(() => client.ExecuteAsync(request, Method.Options)); + + /// + /// Execute the request using OPTIONS HTTP method. Exception will be thrown if the request does not succeed. + /// + /// The request + /// Cancellation token + /// + public async Task OptionsAsync(RestRequest request, CancellationToken cancellationToken = default) { + var response = await client.ExecuteAsync(request, Method.Options, cancellationToken).ConfigureAwait(false); + return response.ThrowIfError(); + } + + /// + /// Execute the request using OPTIONS HTTP method. Exception will be thrown if the request does not succeed. + /// + /// The request + /// + public RestResponse Options(RestRequest request) => AsyncHelpers.RunSync(() => client.OptionsAsync(request)); + + /// + /// Execute the request using OPTIONS HTTP method. Exception will be thrown if the request does not succeed. + /// The response data is deserialized to the Data property of the returned response object. + /// + /// The request + /// Cancellation token + /// Expected result type + /// + public async Task OptionsAsync(RestRequest request, CancellationToken cancellationToken = default) { + var response = await client.ExecuteAsync(request, Method.Options, cancellationToken).ConfigureAwait(false); + return response.ThrowIfError().Data; + } + + /// + /// Execute the request using OPTIONS HTTP method. Exception will be thrown if the request does not succeed. + /// The response data is deserialized to the Data property of the returned response object. + /// + /// The request + /// Expected result type + /// + public T? Options(RestRequest request) => AsyncHelpers.RunSync(() => client.OptionsAsync(request)); + } +} diff --git a/src/RestSharp/RestClient.Extensions.Params.cs b/src/RestSharp/RestClient.Extensions.Params.cs new file mode 100644 index 000000000..cd3bb635c --- /dev/null +++ b/src/RestSharp/RestClient.Extensions.Params.cs @@ -0,0 +1,94 @@ +// 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. +// + +namespace RestSharp; + +public static partial class RestClientExtensions { + /// instance + extension(IRestClient client) { + /// + /// Add a parameter to use on every request made with this client instance + /// + /// to add + /// + public IRestClient AddDefaultParameter(Parameter parameter) { + client.DefaultParameters.AddParameter(parameter); + return client; + } + + /// + /// Adds a default HTTP parameter (QueryString for GET, DELETE, OPTIONS and HEAD; Encoded form for POST and PUT) + /// Used on every request made by this client instance + /// + /// Name of the parameter + /// Value of the parameter + /// This request + public IRestClient AddDefaultParameter(string name, string value) + => client.AddDefaultParameter(new GetOrPostParameter(name, value)); + + /// + /// Adds a default parameter to the client options. There are four types of parameters: + /// - GetOrPost: Either a QueryString value or encoded form value based on method + /// - HttpHeader: Adds the name/value pair to the HTTP request's Headers collection + /// - UrlSegment: Inserted into URL if there is a matching url token e.g. {AccountId} + /// - RequestBody: Used by AddBody() (not recommended to use directly) + /// Used on every request made by this client instance + /// + /// Name of the parameter + /// Value of the parameter + /// The type of parameter to add + /// This request + public IRestClient AddDefaultParameter(string name, object value, ParameterType type) + => client.AddDefaultParameter(Parameter.CreateParameter(name, value, type)); + + /// + /// Adds a default header to the RestClient. Used on every request made by this client instance. + /// + /// Name of the header to add + /// Value of the header to add + /// + public IRestClient AddDefaultHeader(string name, string value) + => client.AddDefaultParameter(new HeaderParameter(name, value)); + + /// + /// Adds default headers to the RestClient. Used on every request made by this client instance. + /// + /// Dictionary containing the Names and Values of the headers to add + /// + public IRestClient AddDefaultHeaders(Dictionary headers) { + foreach (var header in headers) client.AddDefaultParameter(new HeaderParameter(header.Key, header.Value)); + + return client; + } + + /// + /// Adds a default URL segment parameter to the RestClient. Used on every request made by this client instance. + /// + /// Name of the segment to add + /// Value of the segment to add + /// + public IRestClient AddDefaultUrlSegment(string name, string value) + => client.AddDefaultParameter(new UrlSegmentParameter(name, value)); + + /// + /// Adds a default URL query parameter to the RestClient. Used on every request made by this client instance. + /// + /// Name of the query parameter to add + /// Value of the query parameter to add + /// + public IRestClient AddDefaultQueryParameter(string name, string value) + => client.AddDefaultParameter(new QueryParameter(name, value)); + } +} diff --git a/src/RestSharp/RestClient.Extensions.Patch.cs b/src/RestSharp/RestClient.Extensions.Patch.cs new file mode 100644 index 000000000..129f5fc19 --- /dev/null +++ b/src/RestSharp/RestClient.Extensions.Patch.cs @@ -0,0 +1,100 @@ +// 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. +// + +namespace RestSharp; + +public static partial class RestClientExtensions { + /// + extension(IRestClient client) { + /// + /// Executes a PUT-style request asynchronously, authenticating if needed. + /// The response content then gets deserialized to T. + /// + /// Target deserialization type + /// Request to be executed + /// The cancellation token + /// Deserialized response content + public Task> ExecutePatchAsync( + RestRequest request, + CancellationToken cancellationToken = default + ) + => client.ExecuteAsync(request, Method.Patch, cancellationToken); + + /// + /// Executes a PATCH-style request synchronously, authenticating if needed. + /// The response content then gets deserialized to T. + /// + /// Target deserialization type + /// Request to be executed + /// Deserialized response content + public RestResponse ExecutePatch(RestRequest request) => AsyncHelpers.RunSync(() => client.ExecuteAsync(request, Method.Patch)); + + /// + /// Executes a PATCH-style request asynchronously, authenticating if needed + /// + /// Request to be executed + /// Cancellation token + public Task ExecutePatchAsync(RestRequest request, CancellationToken cancellationToken = default) + => client.ExecuteAsync(request, Method.Patch, cancellationToken); + + /// + /// Executes a PATCH-style synchronously, authenticating if needed + /// + /// Request to be executed + public RestResponse ExecutePatch(RestRequest request) => AsyncHelpers.RunSync(() => client.ExecuteAsync(request, Method.Patch)); + + /// + /// Execute the request using PATCH HTTP method. Exception will be thrown if the request does not succeed. + /// The response data is deserialized to the Data property of the returned response object. + /// + /// The request + /// Cancellation token + /// Expected result type + /// + public async Task PatchAsync(RestRequest request, CancellationToken cancellationToken = default) { + var response = await client.ExecuteAsync(request, Method.Patch, cancellationToken).ConfigureAwait(false); + return response.ThrowIfError().Data; + } + + /// + /// Execute the request using PATCH HTTP method. Exception will be thrown if the request does not succeed. + /// The response data is deserialized to the Data property of the returned response object. + /// + /// The request + /// Expected result type + /// + [PublicAPI] + public T? Patch(RestRequest request) => AsyncHelpers.RunSync(() => client.PatchAsync(request)); + + /// + /// Execute the request using PATCH HTTP method. Exception will be thrown if the request does not succeed. + /// + /// The request + /// Cancellation token + /// + public async Task PatchAsync(RestRequest request, CancellationToken cancellationToken = default) { + var response = await client.ExecuteAsync(request, Method.Patch, cancellationToken).ConfigureAwait(false); + return response.ThrowIfError(); + } + + /// + /// Execute the request using PATCH HTTP method. Exception will be thrown if the request does not succeed. + /// + /// The request + /// + [PublicAPI] + public RestResponse Patch(RestRequest request) => AsyncHelpers.RunSync(() => client.PatchAsync(request)); + } +} \ No newline at end of file diff --git a/src/RestSharp/RestClient.Extensions.Post.cs b/src/RestSharp/RestClient.Extensions.Post.cs new file mode 100644 index 000000000..b6eb436f0 --- /dev/null +++ b/src/RestSharp/RestClient.Extensions.Post.cs @@ -0,0 +1,159 @@ +// 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. +// + +namespace RestSharp; + +public static partial class RestClientExtensions { + /// + extension(IRestClient client) { + /// + /// Executes a POST-style request asynchronously, authenticating if needed. + /// The response content then gets deserialized to T. + /// + /// Target deserialization type + /// Request to be executed + /// The cancellation token + /// Deserialized response content + public Task> ExecutePostAsync( + RestRequest request, + CancellationToken cancellationToken = default + ) + => client.ExecuteAsync(request, Method.Post, cancellationToken); + + /// + /// Executes a POST-style request synchronously, authenticating if needed. + /// The response content then gets deserialized to T. + /// + /// Target deserialization type + /// Request to be executed + /// Deserialized response content + public RestResponse ExecutePost(RestRequest request) => AsyncHelpers.RunSync(() => client.ExecuteAsync(request, Method.Post)); + + /// + /// Executes a POST-style asynchronously, authenticating if needed + /// + /// Request to be executed + /// Cancellation token + public Task ExecutePostAsync(RestRequest request, CancellationToken cancellationToken = default) + => client.ExecuteAsync(request, Method.Post, cancellationToken); + + /// + /// Executes a POST-style synchronously, authenticating if needed + /// + /// Request to be executed + public RestResponse ExecutePost(RestRequest request) => AsyncHelpers.RunSync(() => client.ExecuteAsync(request, Method.Post)); + + /// + /// Execute the request using POST HTTP method. Exception will be thrown if the request does not succeed. + /// The response data is deserialized to the Data property of the returned response object. + /// + /// The request + /// Cancellation token + /// Expected result type + /// + public async Task PostAsync(RestRequest request, CancellationToken cancellationToken = default) { + var response = await client.ExecutePostAsync(request, cancellationToken).ConfigureAwait(false); + return response.ThrowIfError().Data; + } + + /// + /// Execute the request using POST HTTP method. Exception will be thrown if the request does not succeed. + /// The response data is deserialized to the Data property of the returned response object. + /// + /// The request + /// Expected result type + /// + public T? Post(RestRequest request) => AsyncHelpers.RunSync(() => client.PostAsync(request)); + + /// + /// Execute the request using POST HTTP method. Exception will be thrown if the request does not succeed. + /// + /// The request + /// Cancellation token + /// + public async Task PostAsync(RestRequest request, CancellationToken cancellationToken = default) { + var response = await client.ExecutePostAsync(request, cancellationToken).ConfigureAwait(false); + return response.ThrowIfError(); + } + + /// + /// Execute the request using POST HTTP method. Exception will be thrown if the request does not succeed. + /// + /// The request + /// + public RestResponse Post(RestRequest request) => AsyncHelpers.RunSync(() => client.PostAsync(request)); + + /// + /// Serializes the request object to JSON and makes a POST call to the resource specified in the resource parameter. + /// Expects a JSON response back, deserializes it to TResponse type and returns it. + /// + /// Resource URL + /// Request object, must be serializable to JSON + /// Cancellation token + /// Request object type + /// Response object type + /// Deserialized response object + public Task PostJsonAsync( + string resource, + TRequest request, + CancellationToken cancellationToken = default + ) where TRequest : class { + var restRequest = new RestRequest(resource).AddJsonBody(request); + return client.PostAsync(restRequest, cancellationToken); + } + + /// + /// Serializes the request object to JSON and makes a POST call to the resource specified in the resource parameter. + /// Expects a JSON response back, deserializes it to TResponse type and returns it. + /// + /// Resource URL + /// Request object, must be serializable to JSON + /// Request object type + /// Response object type + /// Deserialized response object + public TResponse? PostJson(string resource, TRequest request) where TRequest : class + => AsyncHelpers.RunSync(() => client.PostJsonAsync(resource, request)); + + /// + /// Serializes the request object to JSON and makes a POST call to the resource specified in the resource parameter. + /// Expects no response back, just the status code. + /// + /// Resource URL + /// Request object, must be serializable to JSON + /// Cancellation token + /// Request object type + /// Response status code + public async Task PostJsonAsync( + string resource, + TRequest request, + CancellationToken cancellationToken = default + ) where TRequest : class { + var restRequest = new RestRequest(resource).AddJsonBody(request); + var response = await client.PostAsync(restRequest, cancellationToken).ConfigureAwait(false); + return response.StatusCode; + } + + /// + /// Serializes the request object to JSON and makes a POST call to the resource specified in the resource parameter. + /// Expects no response back, just the status code. + /// + /// Resource URL + /// Request object, must be serializable to JSON + /// Request object type + /// Response status code + public HttpStatusCode PostJson(string resource, TRequest request) where TRequest : class + => AsyncHelpers.RunSync(() => client.PostJsonAsync(resource, request)); + } +} \ No newline at end of file diff --git a/src/RestSharp/RestClient.Extensions.Put.cs b/src/RestSharp/RestClient.Extensions.Put.cs new file mode 100644 index 000000000..e404e4e34 --- /dev/null +++ b/src/RestSharp/RestClient.Extensions.Put.cs @@ -0,0 +1,159 @@ +// 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. +// + +namespace RestSharp; + +public static partial class RestClientExtensions { + /// + extension(IRestClient client) { + /// + /// Executes a PUT-style request asynchronously, authenticating if needed. + /// The response content then gets deserialized to T. + /// + /// Target deserialization type + /// Request to be executed + /// The cancellation token + /// Deserialized response content + public Task> ExecutePutAsync( + RestRequest request, + CancellationToken cancellationToken = default + ) + => client.ExecuteAsync(request, Method.Put, cancellationToken); + + /// + /// Executes a PUT-style request synchronously, authenticating if needed. + /// The response content then gets deserialized to T. + /// + /// Target deserialization type + /// Request to be executed + /// Deserialized response content + public RestResponse ExecutePut(RestRequest request) => AsyncHelpers.RunSync(() => client.ExecuteAsync(request, Method.Put)); + + /// + /// Executes a PUP-style request asynchronously, authenticating if needed + /// + /// Request to be executed + /// Cancellation token + public Task ExecutePutAsync(RestRequest request, CancellationToken cancellationToken = default) + => client.ExecuteAsync(request, Method.Put, cancellationToken); + + /// + /// Executes a PUT-style synchronously, authenticating if needed + /// + /// Request to be executed + public RestResponse ExecutePut(RestRequest request) => AsyncHelpers.RunSync(() => client.ExecuteAsync(request, Method.Put)); + + /// + /// Execute the request using PUT HTTP method. Exception will be thrown if the request does not succeed. + /// The response data is deserialized to the Data property of the returned response object. + /// + /// The request + /// Cancellation token + /// Expected result type + /// Deserialaized response + public async Task PutAsync(RestRequest request, CancellationToken cancellationToken = default) { + var response = await client.ExecuteAsync(request, Method.Put, cancellationToken).ConfigureAwait(false); + return response.ThrowIfError().Data; + } + + /// + /// Execute the request using PUT HTTP method. Exception will be thrown if the request does not succeed. + /// The response data is deserialized to the Data property of the returned response object. + /// + /// The request + /// Expected result type + /// + public T? Put(RestRequest request) => AsyncHelpers.RunSync(() => client.PutAsync(request)); + + /// + /// Execute the request using PUT HTTP method. Exception will be thrown if the request does not succeed. + /// + /// The request + /// Cancellation token + /// + public async Task PutAsync(RestRequest request, CancellationToken cancellationToken = default) { + var response = await client.ExecuteAsync(request, Method.Put, cancellationToken).ConfigureAwait(false); + return response.ThrowIfError(); + } + + /// + /// Execute the request using PUT HTTP method. Exception will be thrown if the request does not succeed. + /// + /// The request + /// + public RestResponse Put(RestRequest request) => AsyncHelpers.RunSync(() => client.PutAsync(request)); + + /// + /// Serializes the request object to JSON and makes a PUT call to the resource specified in the resource parameter. + /// Expects a JSON response back, deserializes it to TResponse type and returns it. + /// + /// Resource URL + /// Request object, must be serializable to JSON + /// Cancellation token + /// Request object type + /// Response object type + /// Deserialized response object + public Task PutJsonAsync( + string resource, + TRequest request, + CancellationToken cancellationToken = default + ) where TRequest : class { + var restRequest = new RestRequest(resource).AddJsonBody(request); + return client.PutAsync(restRequest, cancellationToken); + } + + /// + /// Serializes the request object to JSON and makes a PUT call to the resource specified in the resource parameter. + /// Expects a JSON response back, deserializes it to TResponse type and returns it. + /// + /// Resource URL + /// Request object, must be serializable to JSON + /// Request object type + /// Response object type + /// Deserialized response object + public TResponse? PutJson(string resource, TRequest request) where TRequest : class + => AsyncHelpers.RunSync(() => client.PutJsonAsync(resource, request)); + + /// + /// Serializes the request object to JSON and makes a PUT call to the resource specified in the resource parameter. + /// Expects no response back, just the status code. + /// + /// Resource URL + /// Request object, must be serializable to JSON + /// Cancellation token + /// Request object type + /// Response status code + public async Task PutJsonAsync( + string resource, + TRequest request, + CancellationToken cancellationToken = default + ) where TRequest : class { + var restRequest = new RestRequest(resource).AddJsonBody(request); + var response = await client.PutAsync(restRequest, cancellationToken).ConfigureAwait(false); + return response.StatusCode; + } + + /// + /// Serializes the request object to JSON and makes a PUT call to the resource specified in the resource parameter. + /// Expects no response back, just the status code. + /// + /// Resource URL + /// Request object, must be serializable to JSON + /// Request object type + /// Response status code + public HttpStatusCode PutJson(string resource, TRequest request) where TRequest : class + => AsyncHelpers.RunSync(() => client.PutJsonAsync(resource, request)); + } +} \ No newline at end of file diff --git a/src/RestSharp/RestClient.Extensions.cs b/src/RestSharp/RestClient.Extensions.cs new file mode 100644 index 000000000..80d1263f0 --- /dev/null +++ b/src/RestSharp/RestClient.Extensions.cs @@ -0,0 +1,186 @@ +// 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. + +using System.Runtime.CompilerServices; +using RestSharp.Extensions; + +namespace RestSharp; + +[PublicAPI] +public static partial class RestClientExtensions { + /// + extension(IRestClient client) { + [PublicAPI] + [Obsolete("Please use the async overload with a cancellation token")] + public RestResponse Deserialize(RestResponse response) + => AsyncHelpers.RunSync(() + => client.Serializers.Deserialize(response.Request, response, client.Options, CancellationToken.None).AsTask() + ); + + [PublicAPI] + public ValueTask> Deserialize(RestResponse response, CancellationToken cancellationToken) + => client.Serializers.Deserialize(response.Request, response, client.Options, cancellationToken); + + /// + /// Executes the request asynchronously, authenticating if needed + /// + /// Target deserialization type + /// Request to be executed + /// Cancellation token + public async Task> ExecuteAsync( + RestRequest request, + CancellationToken cancellationToken = default + ) { + Ensure.NotNull(request, nameof(request)); + + var response = await client.ExecuteAsync(request, cancellationToken).ConfigureAwait(false); + return await client.Serializers.Deserialize(request, response, client.Options, cancellationToken); + } + + /// + /// Executes the request synchronously, authenticating if needed + /// + /// Target deserialization type + /// Request to be executed + public RestResponse Execute(RestRequest request) => AsyncHelpers.RunSync(() => client.ExecuteAsync(request)); + + /// + /// Executes the request synchronously, authenticating if needed + /// + /// Request to be executed + public RestResponse Execute(RestRequest request) => AsyncHelpers.RunSync(() => client.ExecuteAsync(request)); + + /// + /// Executes the request asynchronously, authenticating if needed + /// + /// Request to be executed + /// Override the request method + /// Cancellation token + public Task ExecuteAsync( + RestRequest request, + Method httpMethod, + CancellationToken cancellationToken = default + ) { + Ensure.NotNull(request, nameof(request)); + + request.Method = httpMethod; + return client.ExecuteAsync(request, cancellationToken); + } + + /// + /// Executes the request synchronously, authenticating if needed + /// + /// Request to be executed + /// Override the request method + public RestResponse Execute(RestRequest request, Method httpMethod) => AsyncHelpers.RunSync(() => client.ExecuteAsync(request, httpMethod)); + + /// + /// Executes the request asynchronously, authenticating if needed + /// + /// Target deserialization type + /// Request to be executed + /// Override the request method + /// Cancellation token + public Task> ExecuteAsync( + RestRequest request, + Method httpMethod, + CancellationToken cancellationToken = default + ) { + Ensure.NotNull(request, nameof(request)); + + request.Method = httpMethod; + return client.ExecuteAsync(request, cancellationToken); + } + + /// + /// Executes the request synchronously, authenticating if needed + /// + /// Target deserialization type + /// Request to be executed + /// Override the request method + public RestResponse Execute(RestRequest request, Method httpMethod) + => AsyncHelpers.RunSync(() => client.ExecuteAsync(request, httpMethod)); + + /// + /// A specialized method to download files. + /// + /// Pre-configured request instance. + /// + /// The downloaded file. + [PublicAPI] + public async Task DownloadDataAsync(RestRequest request, CancellationToken cancellationToken = default) { +#if NET + await using var stream = await client.DownloadStreamAsync(request, cancellationToken).ConfigureAwait(false); +#else + using var stream = await client.DownloadStreamAsync(request, cancellationToken).ConfigureAwait(false); +#endif + return stream == null ? null : await stream.ReadAsBytes(cancellationToken).ConfigureAwait(false); + } + + /// + /// A specialized method to download files as streams. + /// + /// Pre-configured request instance. + /// The cancellation token + /// The downloaded stream. + [PublicAPI] + public Stream? DownloadStream(RestRequest request, CancellationToken cancellationToken = default) + => AsyncHelpers.RunSync(() => client.DownloadStreamAsync(request, cancellationToken)); + + /// + /// A specialized method to download files. + /// + /// Pre-configured request instance. + /// The downloaded file. + public byte[]? DownloadData(RestRequest request) => AsyncHelpers.RunSync(() => client.DownloadDataAsync(request)); + + /// + /// Reads a stream returned by the specified endpoint, deserializes each line to JSON and returns each object asynchronously. + /// It is required for each JSON object to be returned in a single line. + /// + /// + /// + /// + /// + [PublicAPI] + public async IAsyncEnumerable StreamJsonAsync( + string resource, + [EnumeratorCancellation] CancellationToken cancellationToken + ) { + var request = new RestRequest(resource); + +#if NET + await using var stream = await client.DownloadStreamAsync(request, cancellationToken).ConfigureAwait(false); +#else + using var stream = await client.DownloadStreamAsync(request, cancellationToken).ConfigureAwait(false); +#endif + if (stream == null) yield break; + + var serializer = client.Serializers.GetSerializer(DataFormat.Json); + + using var reader = new StreamReader(stream); + +#if NET7_0_OR_GREATER + while (await reader.ReadLineAsync(cancellationToken) is { } line && !cancellationToken.IsCancellationRequested) { +#else + while (await reader.ReadLineAsync() is { } line && !cancellationToken.IsCancellationRequested) { +#endif + if (string.IsNullOrWhiteSpace(line)) continue; + + var response = new RestResponse(request) { Content = line }; + yield return serializer.Deserializer.Deserialize(response)!; + } + } + } +} \ No newline at end of file diff --git a/src/RestSharp/RestClient.cs b/src/RestSharp/RestClient.cs new file mode 100644 index 000000000..c48c45544 --- /dev/null +++ b/src/RestSharp/RestClient.cs @@ -0,0 +1,299 @@ +// 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. + +using System.Diagnostics.CodeAnalysis; +using System.Net.Http.Headers; +using System.Runtime.CompilerServices; +using RestSharp.Serializers; + +// ReSharper disable InvertIf + +// ReSharper disable VirtualMemberCallInConstructor +#pragma warning disable 618 + +namespace RestSharp; + +public delegate void ConfigureHeaders(HttpRequestHeaders headers); + +public delegate void ConfigureSerialization(SerializerConfig config); + +public delegate void ConfigureRestClient(RestClientOptions options); + +/// +/// Client to translate RestRequests into Http requests and process response result +/// +// ReSharper disable once ClassWithVirtualMembersNeverInherited.Global +public partial class RestClient : IRestClient { + /// + /// Content types that will be sent in the Accept header. The list is populated from the known serializers. + /// If you need to send something else by default, set this property to a different value. + /// + public string[] AcceptedContentTypes { + get; + [MethodImpl(MethodImplOptions.Synchronized)] + set; + } + + internal HttpClient HttpClient { get; } + + /// > + public ReadOnlyRestClientOptions Options { get; } + + /// > + public RestSerializers Serializers { get; private set; } + + /// + public DefaultParameters DefaultParameters { get; } + + /// + /// Creates an instance of RestClient using the provided + /// + /// Client options + /// Delegate to add default headers to the wrapped HttpClient instance + /// Delegate to configure serialization + /// Set to true if you wish to reuse the instance + public RestClient( + RestClientOptions options, + ConfigureHeaders? configureDefaultHeaders = null, + ConfigureSerialization? configureSerialization = null, + bool useClientFactory = false + ) { + if (useClientFactory && options.BaseUrl == null) { + throw new ArgumentException("BaseUrl must be set when using a client factory"); + } + + ConfigureSerializers(configureSerialization); + Options = new(options); + DefaultParameters = new(Options); + + if (useClientFactory) { + _disposeHttpClient = false; + HttpClient = SimpleClientFactory.GetClient(options.BaseUrl!, GetClient); + } + else { + _disposeHttpClient = true; + HttpClient = GetClient(); + } + + return; + + HttpClient GetClient() { + var handler = new HttpClientHandler(); + ConfigureHttpMessageHandler(handler, options); + var finalHandler = options.ConfigureMessageHandler?.Invoke(handler) ?? handler; + var httpClient = new HttpClient(finalHandler); + ConfigureHttpClient(httpClient, options); + + // We will use Options.Timeout in ExecuteAsInternalAsync method + httpClient.Timeout = Timeout.InfiniteTimeSpan; + + ConfigureDefaultParameters(options); + configureDefaultHeaders?.Invoke(httpClient.DefaultRequestHeaders); + return httpClient; + } + } + + static RestClientOptions ConfigureOptions(RestClientOptions options, ConfigureRestClient? configureRestClient) { + configureRestClient?.Invoke(options); + return options; + } + + /// + /// Creates an instance of RestClient using the default + /// + /// Delegate to configure the client options + /// Delegate to add default headers to the wrapped HttpClient instance + /// Delegate to configure serialization + /// Set to true if you wish to reuse the instance + public RestClient( + ConfigureRestClient? configureRestClient = null, + ConfigureHeaders? configureDefaultHeaders = null, + ConfigureSerialization? configureSerialization = null, + bool useClientFactory = false + ) : this(ConfigureOptions(new(), configureRestClient), configureDefaultHeaders, configureSerialization, useClientFactory) { } + + /// + /// + /// Creates an instance of RestClient using a specific BaseUrl for requests made by this client instance + /// + /// Base URI for the new client + /// Delegate to configure the client options + /// Delegate to add default headers to the wrapped HttpClient instance + /// Delegate to configure serialization + /// Set to true if you wish to reuse the instance + public RestClient( + Uri baseUrl, + ConfigureRestClient? configureRestClient = null, + ConfigureHeaders? configureDefaultHeaders = null, + ConfigureSerialization? configureSerialization = null, + bool useClientFactory = false + ) + : this( + ConfigureOptions(new() { BaseUrl = baseUrl }, configureRestClient), + configureDefaultHeaders, + configureSerialization, + useClientFactory + ) { } + + /// + /// Creates an instance of RestClient using a specific BaseUrl for requests made by this client instance + /// + /// Base URI for this new client as a string + /// Delegate to configure the client options + /// Delegate to add default headers to the wrapped HttpClient instance + /// Delegate to configure serialization + public RestClient( + string baseUrl, + ConfigureRestClient? configureRestClient = null, + ConfigureHeaders? configureDefaultHeaders = null, + ConfigureSerialization? configureSerialization = null + ) + : this(new Uri(Ensure.NotEmptyString(baseUrl, nameof(baseUrl))), configureRestClient, configureDefaultHeaders, configureSerialization) { } + + /// + /// Creates an instance of RestClient using a shared HttpClient and specific RestClientOptions and does not allocate one internally. + /// + /// HttpClient to use + /// RestClient options to use + /// True to dispose of the client, false to assume the caller does (defaults to false) + /// Delegate to configure serialization + public RestClient( + HttpClient httpClient, + RestClientOptions? options, + bool disposeHttpClient = false, + ConfigureSerialization? configureSerialization = null + ) { + ConfigureSerializers(configureSerialization); + + HttpClient = httpClient; + _disposeHttpClient = disposeHttpClient; + + if (httpClient.BaseAddress != null && options != null && options.BaseUrl == null) { + options.BaseUrl = httpClient.BaseAddress; + } + + var opt = options ?? new RestClientOptions(); + Options = new(opt); + DefaultParameters = new(Options); + + if (options != null) { + ConfigureHttpClient(httpClient, options); + ConfigureDefaultParameters(options); + } + } + + /// + /// Creates an instance of RestClient using a shared HttpClient and does not allocate one internally. + /// + /// HttpClient to use + /// True to dispose of the client, false to assume the caller does (defaults to false) + /// Delegate to configure the client options + /// Delegate to configure serialization + public RestClient( + HttpClient httpClient, + bool disposeHttpClient = false, + ConfigureRestClient? configureRestClient = null, + ConfigureSerialization? configureSerialization = null + ) + : this(httpClient, ConfigureOptions(new(), configureRestClient), disposeHttpClient, configureSerialization) { } + + /// + /// Creates a new instance of RestSharp using the message handler provided. By default, HttpClient disposes the provided handler + /// when the client itself is disposed. If you want to keep the handler not disposed, set disposeHandler argument to false. + /// + /// Message handler instance to use for HttpClient + /// Dispose the handler when disposing RestClient, true by default + /// Delegate to configure the client options + /// Delegate to configure serialization + public RestClient( + HttpMessageHandler handler, + bool disposeHandler = true, + ConfigureRestClient? configureRestClient = null, + ConfigureSerialization? configureSerialization = null + ) + : this(new HttpClient(handler, disposeHandler), true, configureRestClient, configureSerialization) { } + + internal static void ConfigureHttpClient(HttpClient httpClient, RestClientOptions options) { + if (options.Expect100Continue != null) httpClient.DefaultRequestHeaders.ExpectContinue = options.Expect100Continue; + } + + // ReSharper disable once CognitiveComplexity + internal static void ConfigureHttpMessageHandler(HttpClientHandler handler, RestClientOptions options) { +#if NET + if (!OperatingSystem.IsBrowser()) { +#endif + handler.UseCookies = false; + handler.Credentials = options.Credentials; + handler.UseDefaultCredentials = options.UseDefaultCredentials; + handler.AutomaticDecompression = options.AutomaticDecompression; + handler.PreAuthenticate = options.PreAuthenticate; + if (options.MaxRedirects.HasValue) handler.MaxAutomaticRedirections = options.MaxRedirects.Value; + + if (options.RemoteCertificateValidationCallback != null) + handler.ServerCertificateCustomValidationCallback = + (request, cert, chain, errors) => options.RemoteCertificateValidationCallback(request, cert, chain, errors); + + if (options.ClientCertificates != null) { + handler.ClientCertificates.AddRange(options.ClientCertificates); + handler.ClientCertificateOptions = ClientCertificateOption.Manual; + } +#if NET + } +#endif + handler.AllowAutoRedirect = options.FollowRedirects; + +#if NET + // ReSharper disable once InvertIf + if (!OperatingSystem.IsBrowser() && !OperatingSystem.IsIOS() && !OperatingSystem.IsTvOS()) { +#endif + if (handler.SupportsProxy) handler.Proxy = options.Proxy; +#if NET + } +#endif + } + + [MemberNotNull(nameof(Serializers))] + [MemberNotNull(nameof(AcceptedContentTypes))] + void ConfigureSerializers(ConfigureSerialization? configureSerialization) { + var serializerConfig = new SerializerConfig(); + serializerConfig.UseDefaultSerializers(); + configureSerialization?.Invoke(serializerConfig); + Serializers = new(serializerConfig); + AcceptedContentTypes = Serializers.GetAcceptedContentTypes(); + } + + void ConfigureDefaultParameters(RestClientOptions options) { + if (options.UserAgent == null) return; + + if (!options.AllowMultipleDefaultParametersWithSameName && + DefaultParameters.Any(parameter => parameter is { Type: ParameterType.HttpHeader, Name: KnownHeaders.UserAgent })) + DefaultParameters.RemoveParameter(KnownHeaders.UserAgent, ParameterType.HttpHeader); + DefaultParameters.AddParameter(Parameter.CreateParameter(KnownHeaders.UserAgent, options.UserAgent, ParameterType.HttpHeader)); + } + + readonly bool _disposeHttpClient; + bool _disposed; + + protected virtual void Dispose(bool disposing) { + if (!disposing || _disposed) return; + + _disposed = true; + if (_disposeHttpClient) HttpClient.Dispose(); + } + + public void Dispose() { + Dispose(true); + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/src/RestSharp/RestSharp.csproj b/src/RestSharp/RestSharp.csproj new file mode 100644 index 000000000..fced951ab --- /dev/null +++ b/src/RestSharp/RestSharp.csproj @@ -0,0 +1,47 @@ + + + true + + + + + + + + + + + + + + RestClient.cs + + + PropertyCache.cs + + + RestClient.Extensions.cs + + + RestRequestExtensions.cs + + + + + + + + + + + + + + + + + + + + + diff --git a/src/RestSharp/RestSharp.csproj.DotSettings b/src/RestSharp/RestSharp.csproj.DotSettings new file mode 100644 index 000000000..40f3c00e7 --- /dev/null +++ b/src/RestSharp/RestSharp.csproj.DotSettings @@ -0,0 +1,7 @@ + + True + True + True + True + True + True \ No newline at end of file diff --git a/src/RestSharp/Serializers/DeseralizationException.cs b/src/RestSharp/Serializers/DeseralizationException.cs new file mode 100644 index 000000000..6bb193edd --- /dev/null +++ b/src/RestSharp/Serializers/DeseralizationException.cs @@ -0,0 +1,23 @@ +// 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. + +// ReSharper disable once CheckNamespace + +namespace RestSharp; + +public class DeserializationException(RestResponse response, Exception innerException) + : Exception("Error occured while deserializing the response", innerException) { + [PublicAPI] + public RestResponse Response { get; } = response; +} \ No newline at end of file diff --git a/RestSharp/Authenticators/IAuthenticator.cs b/src/RestSharp/Serializers/IDeserializer.cs similarity index 75% rename from RestSharp/Authenticators/IAuthenticator.cs rename to src/RestSharp/Serializers/IDeserializer.cs index 3e340131f..a1332ff50 100644 --- a/RestSharp/Authenticators/IAuthenticator.cs +++ b/src/RestSharp/Serializers/IDeserializer.cs @@ -1,5 +1,4 @@ -#region License -// Copyright 2010 John Sheehan +// 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. @@ -12,12 +11,9 @@ // 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. -#endregion -namespace RestSharp -{ - public interface IAuthenticator - { - void Authenticate(IRestClient client, IRestRequest request); - } -} +namespace RestSharp.Serializers; + +public interface IDeserializer { + T? Deserialize(RestResponse response); +} \ No newline at end of file diff --git a/src/RestSharp/Serializers/IRestSerializer.cs b/src/RestSharp/Serializers/IRestSerializer.cs new file mode 100644 index 000000000..9f1f00fb6 --- /dev/null +++ b/src/RestSharp/Serializers/IRestSerializer.cs @@ -0,0 +1,28 @@ +// 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. + +namespace RestSharp.Serializers; + +public interface IRestSerializer { + ISerializer Serializer { get; } + IDeserializer Deserializer { get; } + + string[] AcceptedContentTypes { get; } + + SupportsContentType SupportsContentType { get; } + + DataFormat DataFormat { get; } + + string? Serialize(Parameter parameter); +} \ No newline at end of file diff --git a/RestSharp/Deserializers/IDeserializer.cs b/src/RestSharp/Serializers/ISerializer.cs similarity index 67% rename from RestSharp/Deserializers/IDeserializer.cs rename to src/RestSharp/Serializers/ISerializer.cs index 844c30d6c..6085f0380 100644 --- a/RestSharp/Deserializers/IDeserializer.cs +++ b/src/RestSharp/Serializers/ISerializer.cs @@ -1,5 +1,4 @@ -#region License -// Copyright 2010 John Sheehan +// 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. @@ -12,15 +11,11 @@ // 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. -#endregion -namespace RestSharp.Deserializers -{ - public interface IDeserializer - { - T Deserialize(IRestResponse response); - string RootElement { get; set; } - string Namespace { get; set; } - string DateFormat { get; set; } - } +namespace RestSharp.Serializers; + +public interface ISerializer { + ContentType ContentType { get; set; } + + string? Serialize(object obj); } diff --git a/src/RestSharp/Serializers/IWithDateFormat.cs b/src/RestSharp/Serializers/IWithDateFormat.cs new file mode 100644 index 000000000..ee693b959 --- /dev/null +++ b/src/RestSharp/Serializers/IWithDateFormat.cs @@ -0,0 +1,19 @@ +// 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. + +namespace RestSharp.Serializers; + +public interface IWithDateFormat { + string? DateFormat { get; set; } +} \ No newline at end of file diff --git a/src/RestSharp/Serializers/IWithRootElement.cs b/src/RestSharp/Serializers/IWithRootElement.cs new file mode 100644 index 000000000..c4cdadca7 --- /dev/null +++ b/src/RestSharp/Serializers/IWithRootElement.cs @@ -0,0 +1,19 @@ +// 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. + +namespace RestSharp.Serializers; + +public interface IWithRootElement { + string? RootElement { get; set; } +} \ No newline at end of file diff --git a/src/RestSharp/Serializers/Json/RestClientExtensions.cs b/src/RestSharp/Serializers/Json/RestClientExtensions.cs new file mode 100644 index 000000000..121f5b635 --- /dev/null +++ b/src/RestSharp/Serializers/Json/RestClientExtensions.cs @@ -0,0 +1,38 @@ +// 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. + +using System.Text.Json; + +namespace RestSharp.Serializers.Json; + +[PublicAPI] +public static class RestClientExtensions { + /// + extension(SerializerConfig serializerConfig) { + /// + /// Use System.Text.Json serializer with default settings + /// + /// + public SerializerConfig UseSystemTextJson() + => serializerConfig.UseSerializer(() => new SystemTextJsonSerializer()); + + /// + /// Use System.Text.Json serializer with custom settings + /// + /// System.Text.Json serializer options + /// + public SerializerConfig UseSystemTextJson(JsonSerializerOptions options) + => serializerConfig.UseSerializer(() => new SystemTextJsonSerializer(options)); + } +} diff --git a/src/RestSharp/Serializers/Json/SystemTextJsonSerializer.cs b/src/RestSharp/Serializers/Json/SystemTextJsonSerializer.cs new file mode 100644 index 000000000..959ffe9e5 --- /dev/null +++ b/src/RestSharp/Serializers/Json/SystemTextJsonSerializer.cs @@ -0,0 +1,46 @@ +// 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. + +using System.Text.Json; + +namespace RestSharp.Serializers.Json; + +public class SystemTextJsonSerializer : IRestSerializer, ISerializer, IDeserializer { + readonly JsonSerializerOptions _options; + + /// + /// Create the new serializer that uses System.Text.Json.JsonSerializer with default settings + /// + public SystemTextJsonSerializer() => _options = new(JsonSerializerDefaults.Web); + + /// + /// Create the new serializer that uses System.Text.Json.JsonSerializer with custom settings + /// + /// Json serializer settings + public SystemTextJsonSerializer(JsonSerializerOptions options) => _options = options; + + public string? Serialize(object? obj) => obj == null ? null : JsonSerializer.Serialize(obj, _options); + + public string? Serialize(Parameter bodyParameter) => Serialize(bodyParameter.Value); + + public T? Deserialize(RestResponse response) => JsonSerializer.Deserialize(response.Content!, _options); + + public ContentType ContentType { get; set; } = ContentType.Json; + + public ISerializer Serializer => this; + public IDeserializer Deserializer => this; + public DataFormat DataFormat => DataFormat.Json; + public string[] AcceptedContentTypes => ContentType.JsonAccept; + public SupportsContentType SupportsContentType => contentType => contentType.Value.EndsWith("json", StringComparison.InvariantCultureIgnoreCase); +} \ No newline at end of file diff --git a/src/RestSharp/Serializers/RestSerializers.cs b/src/RestSharp/Serializers/RestSerializers.cs new file mode 100644 index 000000000..157f5cf19 --- /dev/null +++ b/src/RestSharp/Serializers/RestSerializers.cs @@ -0,0 +1,126 @@ +// 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. +// + +using System.Collections.ObjectModel; +using RestSharp.Extensions; +using RestSharp.Serializers.Xml; + +namespace RestSharp.Serializers; + +public class RestSerializers(Dictionary records) { + [PublicAPI] + public IReadOnlyDictionary Serializers { get; } = new ReadOnlyDictionary(records); + + public RestSerializers(SerializerConfig config) : this(config.Serializers) { } + + public IRestSerializer GetSerializer(DataFormat dataFormat) + => Serializers.TryGetValue(dataFormat, out var value) + ? value.GetSerializer() + : throw new InvalidOperationException($"Unable to find a serializer for {dataFormat}"); + + internal string[] GetAcceptedContentTypes() => Serializers.SelectMany(x => x.Value.AcceptedContentTypes).Distinct().ToArray(); + + internal async ValueTask> Deserialize(RestRequest request, RestResponse raw, ReadOnlyRestClientOptions options, CancellationToken cancellationToken) { + var response = RestResponse.FromResponse(raw); + + try { + await OnBeforeDeserialization(raw, cancellationToken).ConfigureAwait(false); +#pragma warning disable CS0618 // Type or member is obsolete + request.OnBeforeDeserialization?.Invoke(raw); +#pragma warning restore CS0618 // Type or member is obsolete + response.Data = DeserializeContent(raw); + } + catch (Exception ex) { + if (options.ThrowOnAnyError) throw; + + if (options.FailOnDeserializationError || options.ThrowOnDeserializationError) response.ResponseStatus = ResponseStatus.Error; + + response.AddException(ex); + + if (options.ThrowOnDeserializationError) throw new DeserializationException(response, ex); + } + + return response; + } + + static async ValueTask OnBeforeDeserialization(RestResponse response, CancellationToken cancellationToken) { + if (response.Request.Interceptors == null) return; + + foreach (var interceptor in response.Request.Interceptors) { + await interceptor.BeforeDeserialization(response, cancellationToken).ConfigureAwait(false); + } + } + + /// + /// Deserialize the response content into the specified type + /// + /// Response instance + /// Deserialized model type + /// + [PublicAPI] + public T? DeserializeContent(RestResponse response) { + // Only attempt to deserialize if the request has not errored due + // to a transport or framework exception. HTTP errors should attempt to + // be deserialized + if (response.Content == null) { + return default; + } + + // Only continue if there is a handler defined else there is no way to deserialize the data. + // This can happen when a request returns for example a 404 page instead of the requested JSON/XML resource + var deserializer = GetContentDeserializer(response); + + if (deserializer is not IXmlDeserializer xml || response.Request is not RestXmlRequest xmlRequest) + return deserializer != null ? deserializer.Deserialize(response) : default; + + if (xmlRequest.XmlNamespace.IsNotEmpty()) xml.Namespace = xmlRequest.XmlNamespace!; + + if (xml is IWithDateFormat withDateFormat && xmlRequest.DateFormat.IsNotEmpty()) withDateFormat.DateFormat = xmlRequest.DateFormat!; + + return deserializer.Deserialize(response); + } + + IDeserializer? GetContentDeserializer(RestResponseBase response) { + if (string.IsNullOrWhiteSpace(response.Content)) return null; + + var contentType = response.ContentType ?? DetectContentType()?.Value; + + if (contentType == null) { + Serializers.TryGetValue(response.Request.RequestFormat, out var serializerByRequestFormat); + return serializerByRequestFormat?.GetSerializer().Deserializer; + } + + var serializer = Serializers.Values.FirstOrDefault(x => x.SupportsContentType(contentType)); + + // ReSharper disable once InvertIf + if (serializer == null) { + var detectedType = DetectContentType()?.Value; + + if (detectedType != null && detectedType != contentType) + { + serializer = Serializers.Values.FirstOrDefault(x => x.SupportsContentType(detectedType)); + } + } + + return serializer?.GetSerializer().Deserializer; + + ContentType? DetectContentType() + => response.Content![0] switch { + '<' => ContentType.Xml, + '{' or '[' => ContentType.Json, + _ => null + }; + } +} diff --git a/src/RestSharp/Serializers/SerializerConfig.cs b/src/RestSharp/Serializers/SerializerConfig.cs new file mode 100644 index 000000000..27055cf12 --- /dev/null +++ b/src/RestSharp/Serializers/SerializerConfig.cs @@ -0,0 +1,82 @@ +// 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. +// + +using RestSharp.Serializers.Json; +using RestSharp.Serializers.Xml; + +namespace RestSharp.Serializers; + +public class SerializerConfig { + internal Dictionary Serializers { get; } = new(); + + /// + /// Replace the default serializer with a custom one + /// + /// Function that returns the serializer instance + public SerializerConfig UseSerializer(Func serializerFactory) { + var instance = serializerFactory(); + + Serializers[instance.DataFormat] = new( + instance.DataFormat, + instance.AcceptedContentTypes, + instance.SupportsContentType, + serializerFactory + ); + return this; + } + + public void UseDefaultSerializers() => UseSerializer().UseSerializer(); + + /// + /// Replace the default serializer with a custom one + /// + /// The type that implements + /// + public SerializerConfig UseSerializer() where T : class, IRestSerializer, new() => UseSerializer(() => new T()); +} + +public static class SerializerConfigExtensions { + /// Configuration instance to work with + extension(SerializerConfig config) { + /// + /// Sets the to only use JSON + /// + /// Reference to the client instance + public SerializerConfig UseJson() { + config.Serializers.Remove(DataFormat.Xml); + return config; + } + + /// + /// Sets the to only use XML + /// + /// Reference to the client instance + public SerializerConfig UseXml() { + config.Serializers.Remove(DataFormat.Json); + return config; + } + + /// + /// Sets the to only use the passed in custom serializer + /// + /// Function that returns the serializer instance + /// Reference to the client instance + public SerializerConfig UseOnlySerializer(Func serializerFactory) { + config.Serializers.Clear(); + config.UseSerializer(serializerFactory); + return config; + } + } +} diff --git a/src/RestSharp/Serializers/SerializerRecord.cs b/src/RestSharp/Serializers/SerializerRecord.cs new file mode 100644 index 000000000..49ff58630 --- /dev/null +++ b/src/RestSharp/Serializers/SerializerRecord.cs @@ -0,0 +1,22 @@ +// 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. + +namespace RestSharp.Serializers; + +public record SerializerRecord( + DataFormat DataFormat, + string[] AcceptedContentTypes, + SupportsContentType SupportsContentType, + Func GetSerializer +); \ No newline at end of file diff --git a/src/RestSharp/Serializers/Xml/DotNetXmlDeserializer.cs b/src/RestSharp/Serializers/Xml/DotNetXmlDeserializer.cs new file mode 100644 index 000000000..11ce197e4 --- /dev/null +++ b/src/RestSharp/Serializers/Xml/DotNetXmlDeserializer.cs @@ -0,0 +1,51 @@ +// 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. + +using System.Text; + +namespace RestSharp.Serializers.Xml; + +/// +/// Wrapper for System.Xml.Serialization.XmlSerializer. +/// +public class DotNetXmlDeserializer : IXmlDeserializer { + /// + /// Encoding for serialized content + /// + public Encoding Encoding { get; set; } = Encoding.UTF8; + + /// + /// Name of the root element to use when serializing + /// + [Obsolete("DotnetXmlDeserializer does not support RootElement.")] + public string? RootElement { get; set; } + + /// + /// XML namespace to use when serializing + /// + public string? Namespace { get; set; } + + [Obsolete("DotnetXmlDeserializer does not support DateFormat.")] + public string? DateFormat { get; set; } + + public T? Deserialize(RestResponse response) { + if (string.IsNullOrEmpty(response.Content)) return default; + + using var stream = new MemoryStream(Encoding.GetBytes(response.Content!)); + + var serializer = new System.Xml.Serialization.XmlSerializer(typeof(T), Namespace); + + return (T?)serializer.Deserialize(stream); + } +} \ No newline at end of file diff --git a/src/RestSharp/Serializers/Xml/DotNetXmlSerializer.cs b/src/RestSharp/Serializers/Xml/DotNetXmlSerializer.cs new file mode 100644 index 000000000..ef7f2f091 --- /dev/null +++ b/src/RestSharp/Serializers/Xml/DotNetXmlSerializer.cs @@ -0,0 +1,126 @@ +// 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. + +using System.Text; +using System.Xml.Serialization; + +namespace RestSharp.Serializers.Xml; + +/// +/// Wrapper for System.Xml.Serialization.XmlSerializer. +/// +public class DotNetXmlSerializer : IXmlSerializer { + /// + /// Default constructor, does not specify namespace + /// + public DotNetXmlSerializer() { + ContentType = ContentType.Xml; + Encoding = Encoding.UTF8; + } + + /// + /// + /// Specify the namespaced to be used when serializing + /// + /// XML namespace + [PublicAPI] + public DotNetXmlSerializer(string @namespace) : this() => Namespace = @namespace; + + /// + /// Encoding for serialized content + /// + public Encoding Encoding { get; set; } + + /// + /// Serialize the object as XML + /// + /// Object to serialize + /// XML as string + public string Serialize(object obj) { + var ns = new XmlSerializerNamespaces(); + + ns.Add(string.Empty, Namespace!); + + var serializer = GetXmlSerializer(obj.GetType(), RootElement); + var writer = new EncodingStringWriter(Encoding); + + serializer.Serialize(writer, obj, ns); + + return writer.ToString(); + } + + /// + /// Name of the root element to use when serializing + /// + public string? RootElement { get; set; } + + /// + /// XML namespace to use when serializing + /// + public string? Namespace { get; set; } + + /// + /// Content type for serialized content + /// + public ContentType ContentType { get; set; } + + static readonly Dictionary<(Type, string?), XmlSerializer> Cache = new(); + static readonly ReaderWriterLockSlim CacheLock = new(); + + static XmlSerializer GetXmlSerializer(Type type, string? rootElement) { + XmlSerializer? serializer = null; + + var key = (type, rootElement); + + CacheLock.EnterReadLock(); + + try { + if (Cache.TryGetValue(key, out var value)) { + serializer = value; + } + } + finally { + CacheLock.ExitReadLock(); + } + + if (serializer != null) { + return serializer; + } + + CacheLock.EnterWriteLock(); + + try { + // check again for a cached instance, because between the EnterWriteLock + // and the last check, some other thread could have added an instance + if (!Cache.TryGetValue(key, out var value)) { + var root = rootElement == null ? null : new XmlRootAttribute(rootElement); + value = new(type, root); + Cache[key] = value; + } + + serializer = value; + } + finally { + CacheLock.ExitWriteLock(); + } + + return serializer; + } + + class EncodingStringWriter(Encoding encoding) : StringWriter { + // Need to subclass StringWriter in order to override Encoding + + public override Encoding Encoding { get; } = encoding; + } +} \ No newline at end of file diff --git a/src/RestSharp/Serializers/Xml/DotNetXmlSerializerClientExtensions.cs b/src/RestSharp/Serializers/Xml/DotNetXmlSerializerClientExtensions.cs new file mode 100644 index 000000000..2a6d20d64 --- /dev/null +++ b/src/RestSharp/Serializers/Xml/DotNetXmlSerializerClientExtensions.cs @@ -0,0 +1,39 @@ +// 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. + +using System.Text; + +namespace RestSharp.Serializers.Xml; + +[PublicAPI] +public static class DotNetXmlSerializerClientExtensions { + public static SerializerConfig UseDotNetXmlSerializer( + this SerializerConfig serializerConfig, + string? xmlNamespace = null, + Encoding? encoding = null + ) { + var xmlSerializer = new DotNetXmlSerializer(); + if (xmlNamespace != null) xmlSerializer.Namespace = xmlNamespace; + if (encoding != null) xmlSerializer.Encoding = encoding; + + var xmlDeserializer = new DotNetXmlDeserializer(); + if (encoding != null) xmlDeserializer.Encoding = encoding; + + var serializer = new XmlRestSerializer() + .WithXmlSerializer(xmlSerializer) + .WithXmlDeserializer(xmlDeserializer); + + return serializerConfig.UseSerializer(() => serializer); + } +} diff --git a/src/RestSharp/Serializers/Xml/IXmlDeserializer.cs b/src/RestSharp/Serializers/Xml/IXmlDeserializer.cs new file mode 100644 index 000000000..975cce014 --- /dev/null +++ b/src/RestSharp/Serializers/Xml/IXmlDeserializer.cs @@ -0,0 +1,19 @@ +// 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. + +namespace RestSharp.Serializers.Xml; + +public interface IXmlDeserializer : IDeserializer { + string? Namespace { get; set; } +} \ No newline at end of file diff --git a/src/RestSharp/Serializers/Xml/IXmlSerializer.cs b/src/RestSharp/Serializers/Xml/IXmlSerializer.cs new file mode 100644 index 000000000..255c9e0fd --- /dev/null +++ b/src/RestSharp/Serializers/Xml/IXmlSerializer.cs @@ -0,0 +1,19 @@ +// 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. + +namespace RestSharp.Serializers.Xml; + +public interface IXmlSerializer : ISerializer { + string? Namespace { get; set; } +} \ No newline at end of file diff --git a/src/RestSharp/Serializers/Xml/XmlRestSerializer.cs b/src/RestSharp/Serializers/Xml/XmlRestSerializer.cs new file mode 100644 index 000000000..db857095f --- /dev/null +++ b/src/RestSharp/Serializers/Xml/XmlRestSerializer.cs @@ -0,0 +1,56 @@ +// 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. + +namespace RestSharp.Serializers.Xml; + +public class XmlRestSerializer(IXmlSerializer serializer, IXmlDeserializer deserializer) : IRestSerializer { + IXmlDeserializer _deserializer = deserializer; + IXmlSerializer _serializer = serializer; + + public XmlRestSerializer() : this(new DotNetXmlSerializer(), new DotNetXmlDeserializer()) { } + + public ISerializer Serializer => _serializer; + public IDeserializer Deserializer => _deserializer; + public string[] AcceptedContentTypes => ContentType.XmlAccept; + public SupportsContentType SupportsContentType => contentType => contentType.Value.EndsWith("xml", StringComparison.InvariantCultureIgnoreCase); + + public DataFormat DataFormat => DataFormat.Xml; + + public string? Serialize(Parameter parameter) { + if (parameter is not XmlParameter xmlParameter) + throw new ArgumentException("Supplied parameter is not an XML parameter", nameof(parameter)); + + if (parameter.Value == null) + throw new ArgumentNullException(nameof(parameter), "Parameter value is null"); + + var savedNamespace = _serializer.Namespace; + _serializer.Namespace = xmlParameter.XmlNamespace ?? savedNamespace; + + var result = _serializer.Serialize(parameter.Value); + + _serializer.Namespace = savedNamespace; + + return result; + } + + public XmlRestSerializer WithXmlSerializer(IXmlSerializer xmlSerializer) { + _serializer = xmlSerializer; + return this; + } + + public XmlRestSerializer WithXmlDeserializer(IXmlDeserializer xmlDeserializer) { + _deserializer = xmlDeserializer; + return this; + } +} \ No newline at end of file diff --git a/src/RestSharp/SimpleClientFactory.cs b/src/RestSharp/SimpleClientFactory.cs new file mode 100644 index 000000000..09ca395c1 --- /dev/null +++ b/src/RestSharp/SimpleClientFactory.cs @@ -0,0 +1,27 @@ +// 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. +// + +using System.Collections.Concurrent; + +namespace RestSharp; + +static class SimpleClientFactory { + static readonly ConcurrentDictionary CachedClients = new(); + + public static HttpClient GetClient(Uri baseUrl, Func getClient) { + var key = baseUrl.ToString(); + return CachedClients.GetOrAdd(key, _ => getClient()); + } +} diff --git a/test/Directory.Build.props b/test/Directory.Build.props new file mode 100644 index 000000000..2279b675c --- /dev/null +++ b/test/Directory.Build.props @@ -0,0 +1,34 @@ + + + + true + false + net48;net8.0;net9.0;net10.0 + disable + xUnit1033 + trx%3bLogFileName=$(MSBuildProjectName).trx + $(RepoRoot)/test-results/$(TargetFramework) + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/RestSharp.InteractiveTests/AuthenticationTests.cs b/test/RestSharp.InteractiveTests/AuthenticationTests.cs new file mode 100644 index 000000000..7e5d81173 --- /dev/null +++ b/test/RestSharp.InteractiveTests/AuthenticationTests.cs @@ -0,0 +1,84 @@ +using System.Net; +using System.Web; +using RestSharp.Authenticators; + +namespace RestSharp.InteractiveTests; + +public class AuthenticationTests { + public class TwitterKeys { + public string ConsumerKey { get; set; } + public string ConsumerSecret { get; set; } + } + + public static async Task Can_Authenticate_With_OAuth_Async_With_Callback(TwitterKeys twitterKeys) { + Console.WriteLine("OAuth test with callback"); + + var baseUrl = new Uri("https://api.twitter.com"); + + using var client = new RestClient( + baseUrl, + options => + options.Authenticator = OAuth1Authenticator.ForRequestToken( + twitterKeys.ConsumerKey!, + twitterKeys.ConsumerSecret, + "https://restsharp.dev" + ) + ); + var request = new RestRequest("oauth/request_token"); + var response = await client.ExecuteAsync(request); + + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var qs = HttpUtility.ParseQueryString(response.Content); + var oauthToken = qs["oauth_token"]; + var oauthTokenSecret = qs["oauth_token_secret"]; + + Assert.NotNull(oauthToken); + Assert.NotNull(oauthTokenSecret); + + request = new($"oauth/authorize?oauth_token={oauthToken}"); + + var url = client.BuildUri(request) + .ToString(); + + Console.WriteLine($"Open this URL in the browser: {url} and complete the authentication."); + Console.Write("Enter the verifier: "); + var verifier = Console.ReadLine(); + + request = new("oauth/access_token") { + Authenticator = OAuth1Authenticator.ForAccessToken( + twitterKeys.ConsumerKey!, + twitterKeys.ConsumerSecret, + oauthToken!, + oauthTokenSecret!, + verifier! + ) + }; + + response = await client.ExecuteAsync(request); + + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + qs = HttpUtility.ParseQueryString(response.Content); + oauthToken = qs["oauth_token"]; + oauthTokenSecret = qs["oauth_token_secret"]; + + Assert.NotNull(oauthToken); + Assert.NotNull(oauthTokenSecret); + + request = new("1.1/account/verify_credentials.json") { + Authenticator = OAuth1Authenticator.ForProtectedResource( + twitterKeys.ConsumerKey!, + twitterKeys.ConsumerSecret, + oauthToken!, + oauthTokenSecret! + ) + }; + + response = await client.ExecuteAsync(request); + + Console.WriteLine($"Code: {response.StatusCode}, response: {response.Content}"); + } +} diff --git a/test/RestSharp.InteractiveTests/Program.cs b/test/RestSharp.InteractiveTests/Program.cs new file mode 100644 index 000000000..bea2b8123 --- /dev/null +++ b/test/RestSharp.InteractiveTests/Program.cs @@ -0,0 +1,24 @@ +using RestSharp.InteractiveTests; +// ReSharper disable HeuristicUnreachableCode + +using var client = new TwitterClient("apikey", "apisecret"); + +await foreach (var tweet in client.SearchStream()) { + Console.WriteLine(tweet); +} + +return; + +#pragma warning disable CS0162 // Unreachable code detected +var keys = new AuthenticationTests.TwitterKeys { + ConsumerKey = Prompt("Consumer key"), + ConsumerSecret = Prompt("Consumer secret") +}; + +await AuthenticationTests.Can_Authenticate_With_OAuth_Async_With_Callback(keys); +#pragma warning restore CS0162 // Unreachable code detected + +static string Prompt(string message) { + Console.Write($"{message}: "); + return Console.ReadLine(); +} \ No newline at end of file diff --git a/test/RestSharp.InteractiveTests/RestSharp.InteractiveTests.csproj b/test/RestSharp.InteractiveTests/RestSharp.InteractiveTests.csproj new file mode 100644 index 000000000..75c24e34a --- /dev/null +++ b/test/RestSharp.InteractiveTests/RestSharp.InteractiveTests.csproj @@ -0,0 +1,11 @@ + + + Exe + false + net9.0 + + + + + + diff --git a/test/RestSharp.InteractiveTests/TwitterClient.cs b/test/RestSharp.InteractiveTests/TwitterClient.cs new file mode 100644 index 000000000..0df6d0555 --- /dev/null +++ b/test/RestSharp.InteractiveTests/TwitterClient.cs @@ -0,0 +1,112 @@ +// 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. +// + +using System.Runtime.CompilerServices; +using System.Text.Json.Serialization; +using RestSharp.Authenticators; +// ReSharper disable ClassNeverInstantiated.Local + +namespace RestSharp.InteractiveTests; + +public interface ITwitterClient { + Task GetUser(string user); +} + +public class TwitterClient : ITwitterClient, IDisposable { + readonly RestClient _client; + + public TwitterClient(string apiKey, string apiKeySecret) { + var options = new RestClientOptions("https://api.twitter.com/2") { + Authenticator = new TwitterAuthenticator("https://api.twitter.com", apiKey, apiKeySecret) + }; + _client = new(options); + } + + public async Task GetUser(string user) { + var response = await _client.GetAsync>( + "users/by/username/{user}", + new { user } + ); + return response!.Data; + } + + public async Task AddSearchRules(params AddStreamSearchRule[] rules) { + var response = await _client.PostJsonAsync>( + "tweets/search/stream/rules", + new(rules) + ); + return response?.Data; + } + + public async Task GetSearchRules() { + var response = await _client.GetAsync>("tweets/search/stream/rules"); + return response?.Data; + } + + public async IAsyncEnumerable SearchStream([EnumeratorCancellation] CancellationToken cancellationToken = default) { + var response = _client.StreamJsonAsync>("tweets/search/stream", cancellationToken); + + await foreach (var item in response) { + yield return item.Data; + } + } + + record TwitterSingleObject(T Data); + + record TwitterCollectionObject(T[] Data); + + record AddSearchRulesRequest(AddStreamSearchRule[] Add); + + public void Dispose() { + _client?.Dispose(); + GC.SuppressFinalize(this); + } +} + +class TwitterAuthenticator(string baseUrl, string clientId, string clientSecret) : AuthenticatorBase("") { + protected override async ValueTask GetAuthenticationParameter(string accessToken) { + var token = string.IsNullOrEmpty(Token) ? await GetToken() : Token; + Token = token; + return new HeaderParameter(KnownHeaders.Authorization, token); + } + + async Task GetToken() { + var options = new RestClientOptions(baseUrl) { + Authenticator = new HttpBasicAuthenticator(clientId, clientSecret) + }; + + using var client = new RestClient(options); + + var request = new RestRequest("oauth2/token") + .AddParameter("grant_type", "client_credentials"); + var response = await client.PostAsync(request); + return $"{response!.TokenType} {response!.AccessToken}"; + } + + record TokenResponse { + [JsonPropertyName("token_type")] + public string TokenType { get; init; } + [JsonPropertyName("access_token")] + public string AccessToken { get; init; } + } +} + +public record TwitterUser(string Id, string Name, string Username); + +public record AddStreamSearchRule(string Value, string Tag); + +public record SearchRulesResponse(string Value, string Tag, string Id); + +public record SearchResponse(string Id, string Text); diff --git a/test/RestSharp.Tests.DependencyInjection/DefaultClientRequestTests.cs b/test/RestSharp.Tests.DependencyInjection/DefaultClientRequestTests.cs new file mode 100644 index 000000000..9d45b072e --- /dev/null +++ b/test/RestSharp.Tests.DependencyInjection/DefaultClientRequestTests.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.DependencyInjection; +using RestSharp.Extensions.DependencyInjection; +using RestSharp.Tests.Shared; +using RestSharp.Tests.Shared.Server; + +namespace RestSharp.Tests.DependencyInjection; + +public sealed class DefaultClientRequestTests : RequestTestsBase, IClassFixture, IDisposable { + readonly ServiceProvider _provider; + + public DefaultClientRequestTests(WireMockTestServer server) : base(false) { + var services = new ServiceCollection(); + services.AddRestClient(new Uri(server.Url!)); + _provider = services.BuildServiceProvider(); + } + + public void Dispose() => _provider.Dispose(); + + protected override IRestClient GetClient() => _provider.GetRequiredService(); +} \ No newline at end of file diff --git a/test/RestSharp.Tests.DependencyInjection/NamedClientWithBaseUrlRequestTests.cs b/test/RestSharp.Tests.DependencyInjection/NamedClientWithBaseUrlRequestTests.cs new file mode 100644 index 000000000..387fce5c3 --- /dev/null +++ b/test/RestSharp.Tests.DependencyInjection/NamedClientWithBaseUrlRequestTests.cs @@ -0,0 +1,22 @@ +using Microsoft.Extensions.DependencyInjection; +using RestSharp.Extensions.DependencyInjection; +using RestSharp.Tests.Shared; +using RestSharp.Tests.Shared.Server; + +namespace RestSharp.Tests.DependencyInjection; + +public sealed class NamedClientWithBaseUrlRequestTests : RequestTestsBase, IClassFixture, IDisposable { + readonly ServiceProvider _provider; + + const string ClientName = "test"; + + public NamedClientWithBaseUrlRequestTests(WireMockTestServer server) : base(false) { + var services = new ServiceCollection(); + services.AddRestClient(ClientName, new Uri(server.Url!)); + _provider = services.BuildServiceProvider(); + } + + public void Dispose() => _provider.Dispose(); + + protected override IRestClient GetClient() => _provider.GetRequiredService().CreateClient(ClientName); +} \ No newline at end of file diff --git a/test/RestSharp.Tests.DependencyInjection/NamedClientWithOptionsRequestTests.cs b/test/RestSharp.Tests.DependencyInjection/NamedClientWithOptionsRequestTests.cs new file mode 100644 index 000000000..1fda7af5c --- /dev/null +++ b/test/RestSharp.Tests.DependencyInjection/NamedClientWithOptionsRequestTests.cs @@ -0,0 +1,22 @@ +using Microsoft.Extensions.DependencyInjection; +using RestSharp.Extensions.DependencyInjection; +using RestSharp.Tests.Shared; +using RestSharp.Tests.Shared.Server; + +namespace RestSharp.Tests.DependencyInjection; + +public sealed class NamedClientWithOptionsRequestTests : RequestTestsBase, IClassFixture, IDisposable { + readonly ServiceProvider _provider; + + const string ClientName = "test"; + + public NamedClientWithOptionsRequestTests(WireMockTestServer server) : base(false) { + var services = new ServiceCollection(); + services.AddRestClient(ClientName, new RestClientOptions(server.Url!)); + _provider = services.BuildServiceProvider(); + } + + public void Dispose() => _provider.Dispose(); + + protected override IRestClient GetClient() => _provider.GetRequiredService().CreateClient(ClientName); +} \ No newline at end of file diff --git a/test/RestSharp.Tests.DependencyInjection/RestSharp.Tests.DependencyInjection.csproj b/test/RestSharp.Tests.DependencyInjection/RestSharp.Tests.DependencyInjection.csproj new file mode 100644 index 000000000..90202d946 --- /dev/null +++ b/test/RestSharp.Tests.DependencyInjection/RestSharp.Tests.DependencyInjection.csproj @@ -0,0 +1,6 @@ + + + + + + diff --git a/RestSharp.IntegrationTests/Assets/Koala.jpg b/test/RestSharp.Tests.Integrated/Assets/Koala.jpg similarity index 100% rename from RestSharp.IntegrationTests/Assets/Koala.jpg rename to test/RestSharp.Tests.Integrated/Assets/Koala.jpg diff --git "a/test/RestSharp.Tests.Integrated/Assets/Koala\303\204\303\226\303\244\303\266.jpg" "b/test/RestSharp.Tests.Integrated/Assets/Koala\303\204\303\226\303\244\303\266.jpg" new file mode 100644 index 000000000..78704a099 Binary files /dev/null and "b/test/RestSharp.Tests.Integrated/Assets/Koala\303\204\303\226\303\244\303\266.jpg" differ diff --git a/test/RestSharp.Tests.Integrated/Assets/TestFile.txt b/test/RestSharp.Tests.Integrated/Assets/TestFile.txt new file mode 100644 index 000000000..9cde37b4e --- /dev/null +++ b/test/RestSharp.Tests.Integrated/Assets/TestFile.txt @@ -0,0 +1 @@ +This is a test file for RestSharp. \ No newline at end of file diff --git "a/test/RestSharp.Tests.Integrated/Assets/Test\303\245\303\246.txt" "b/test/RestSharp.Tests.Integrated/Assets/Test\303\245\303\246.txt" new file mode 100644 index 000000000..9cde37b4e --- /dev/null +++ "b/test/RestSharp.Tests.Integrated/Assets/Test\303\245\303\246.txt" @@ -0,0 +1 @@ +This is a test file for RestSharp. \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/Authentication/AuthenticationTests.cs b/test/RestSharp.Tests.Integrated/Authentication/AuthenticationTests.cs new file mode 100644 index 000000000..6a8a43df2 --- /dev/null +++ b/test/RestSharp.Tests.Integrated/Authentication/AuthenticationTests.cs @@ -0,0 +1,28 @@ +using System.Text; +using System.Web; +using RestSharp.Authenticators; + +namespace RestSharp.Tests.Integrated.Authentication; + +public class AuthenticationTests(WireMockTestServer server) : IClassFixture { + [Fact] + public async Task Can_Authenticate_With_Basic_Http_Auth() { + const string userName = "testuser"; + const string password = "testpassword"; + + using var client = new RestClient( + server.Url!, + o => o.Authenticator = new HttpBasicAuthenticator(userName, password) + ); + var request = new RestRequest("headers"); + var response = await client.GetAsync(request); + + var header = response!.First(x => x.Name == KnownHeaders.Authorization); + var auth = HttpUtility.UrlDecode(header.Value)["Basic ".Length..]; + var value = Convert.FromBase64String(auth); + var parts = Encoding.UTF8.GetString(value).Split(':'); + + parts[0].Should().Be(userName); + parts[1].Should().Be(password); + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/Authentication/OAuth2Tests.cs b/test/RestSharp.Tests.Integrated/Authentication/OAuth2Tests.cs new file mode 100644 index 000000000..3a15901a3 --- /dev/null +++ b/test/RestSharp.Tests.Integrated/Authentication/OAuth2Tests.cs @@ -0,0 +1,17 @@ +using RestSharp.Authenticators.OAuth2; + +namespace RestSharp.Tests.Integrated.Authentication; + +public class OAuth2Tests(WireMockTestServer server) : IClassFixture { + [Fact] + public async Task ShouldHaveProperHeader() { + var auth = new OAuth2AuthorizationRequestHeaderAuthenticator("token", "Bearer"); + using var client = new RestClient(server.Url!, o => o.Authenticator = auth); + + var response = await client.GetAsync("headers"); + var authHeader = response!.FirstOrDefault(x => x.Name == KnownHeaders.Authorization); + + authHeader.Should().NotBeNull(); + authHeader!.Value.Should().Be("Bearer token"); + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/CompressionTests.cs b/test/RestSharp.Tests.Integrated/CompressionTests.cs new file mode 100644 index 000000000..c20526423 --- /dev/null +++ b/test/RestSharp.Tests.Integrated/CompressionTests.cs @@ -0,0 +1,71 @@ +using System.IO.Compression; +using RestSharp.Extensions; +using RestSharp.Tests.Shared.Extensions; + +namespace RestSharp.Tests.Integrated; + +public class CompressionTests { + static async Task GetBody(Func getStream, string value) { + using var memoryStream = new MemoryStream(); + + // ReSharper disable once UseAwaitUsing + using (var stream = getStream(memoryStream)) { + stream.WriteStringUtf8(value); + } + + memoryStream.Seek(0, SeekOrigin.Begin); + var body = await memoryStream.ReadAsBytes(default); + return body; + } + + static void ConfigureServer(WireMockServer server, byte[] body, string encoding) + => server + .Given(Request.Create().WithPath("/").UsingGet()) + .RespondWith(Response.Create().WithBody(body).WithHeader("Content-Encoding", encoding)); + + [Fact] + public async Task Can_Handle_Deflate_Compressed_Content() { + const string value = "This is some deflated content"; + using var server = WireMockServer.Start(); + + var body = await GetBody(s => new DeflateStream(s, CompressionMode.Compress, true), value); + ConfigureServer(server, body, "deflate"); + + using var client = new RestClient(server.Url!, options => options.AutomaticDecompression = DecompressionMethods.Deflate); + var request = new RestRequest(""); + var response = await client.ExecuteAsync(request); + + response.Content.Should().Be(value); + } + + [Fact] + public async Task Can_Handle_Gzip_Compressed_Content() { + const string value = "This is some gzipped content"; + using var server = WireMockServer.Start(); + + var body = await GetBody(s => new GZipStream(s, CompressionMode.Compress, true), value); + ConfigureServer(server, body, "gzip"); + + using var client = new RestClient(server.Url!); + var request = new RestRequest(""); + var response = await client.ExecuteAsync(request); + + response.Content.Should().Be(value); + } + + [Fact] + public async Task Can_Handle_Uncompressed_Content() { + const string value = "This is some sample content"; + using var server = WireMockServer.Start(); + + server + .Given(Request.Create().WithPath("/").UsingGet()) + .RespondWith(Response.Create().WithBody(value)); + + using var client = new RestClient(server.Url!); + var request = new RestRequest(""); + var response = await client.ExecuteAsync(request); + + response.Content.Should().Be(value); + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/CookieTests.cs b/test/RestSharp.Tests.Integrated/CookieTests.cs new file mode 100644 index 000000000..1014fd3d4 --- /dev/null +++ b/test/RestSharp.Tests.Integrated/CookieTests.cs @@ -0,0 +1,197 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; +using RestSharp.Tests.Integrated.Fixtures; +using WireMock.Types; +using WireMock.Util; + +namespace RestSharp.Tests.Integrated; + +public sealed class CookieTests : IDisposable { + readonly RestClient _client; + readonly string _host; + readonly WireMockServer _server = WireMockServer.Start(); + + public CookieTests() { + var options = new RestClientOptions(_server.Url!) { + CookieContainer = new CookieContainer() + }; + _client = new RestClient(options); + _host = _client.Options.BaseUrl!.Host; + + _server + .Given(Request.Create().WithPath("/get-cookies")) + .RespondWith(Response.Create().WithCallback(HandleGetCookies)); + + _server + .Given(Request.Create().WithPath("/set-cookies")) + .RespondWith(Response.Create().WithCallback(HandleSetCookies)); + + _server + .Given(Request.Create().WithPath("/invalid-cookies")) + .RespondWith(Response.Create().WithCallback(HandleInvalidCookies)); + } + + [Fact] + public async Task Can_Perform_GET_Async_With_Request_Cookies() { + var request = new RestRequest("get-cookies") { + CookieContainer = new CookieContainer() + }; + request.CookieContainer.Add(new Cookie("cookie", "value", null, _host)); + request.CookieContainer.Add(new Cookie("cookie2", "value2", null, _host)); + var response = await _client.ExecuteAsync(request); + response.Content.Should().Be("[\"cookie=value\",\"cookie2=value2\"]"); + } + + [Fact] + public async Task Can_Perform_GET_Async_With_Request_And_Client_Cookies() { + _client.Options.CookieContainer!.Add(new Cookie("clientCookie", "clientCookieValue", null, _host)); + + var request = new RestRequest("get-cookies") { + CookieContainer = new CookieContainer() + }; + request.CookieContainer.Add(new Cookie("cookie", "value", null, _host)); + request.CookieContainer.Add(new Cookie("cookie2", "value2", null, _host)); + var response = await _client.ExecuteAsync(request); + + var expected = new[] { "cookie=value", "cookie2=value2", "clientCookie=clientCookieValue" }; + response.Data.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task Can_Perform_GET_Async_With_Response_Cookies() { + var request = new RestRequest("set-cookies"); + var response = await _client.ExecuteAsync(request); + response.Content.Should().Be("success"); + + AssertCookie("cookie1", "value1", x => x == DateTime.MinValue); + response.Cookies.Find("cookie2").Should().BeNull("Cookie 2 should vanish as the path will not match"); + AssertCookie("cookie3", "value3", x => x > DateTime.UtcNow); + AssertCookie("cookie4", "value4", x => x > DateTime.UtcNow); + response.Cookies.Find("cookie5").Should().BeNull("Cookie 5 should vanish as the request is not SSL"); + AssertCookie("cookie6", "value6", x => x == DateTime.MinValue, true); + return; + + void AssertCookie(string name, string value, Func checkExpiration, bool httpOnly = false) { + var c = response.Cookies.Find(name)!; + c.Value.Should().Be(value); + c.Path.Should().Be("/"); + c.Domain.Should().Be(_host); + checkExpiration(c.Expires).Should().BeTrue($"Expires at {c.Expires}"); + c.HttpOnly.Should().Be(httpOnly); + } + } + + [Fact] + public async Task GET_Async_With_Response_Cookies_Should_Not_Fail_With_Cookie_With_Empty_Domain() { + var request = new RestRequest("set-cookies"); + var response = await _client.ExecuteAsync(request); + response.Content.Should().Be("success"); + + var notFoundCookie = response.Cookies.Find("cookie_empty_domain"); + notFoundCookie.Should().BeNull(); + + var emptyDomainCookieHeader = response + .GetHeaderValues(KnownHeaders.SetCookie) + .SingleOrDefault(h => h.StartsWith("cookie_empty_domain")); + emptyDomainCookieHeader.Should().NotBeNull(); + emptyDomainCookieHeader.Should().Contain("domain=;"); + } + + [Fact] + public async Task GET_Async_With_Invalid_Cookies_Should_Still_Return_Response_When_IgnoreInvalidCookies_Is_False() { + var request = new RestRequest("invalid-cookies") { + CookieContainer = new() + }; + var response = await _client.ExecuteAsync(request); + + // Even with IgnoreInvalidCookies = false, the response should be successful + // because AddCookies swallows CookieException by default + response.IsSuccessful.Should().BeTrue(); + response.Content.Should().Be("success"); + } + + [Fact] + public async Task GET_Async_With_Invalid_Cookies_Should_Return_Response_When_IgnoreInvalidCookies_Is_True() { + var options = new RestClientOptions(_server.Url!) { + CookieContainer = new() + }; + using var client = new RestClient(options); + + var request = new RestRequest("invalid-cookies"); + var response = await client.ExecuteAsync(request); + + response.IsSuccessful.Should().BeTrue(); + response.Content.Should().Be("success"); + } + + static ResponseMessage HandleGetCookies(IRequestMessage request) { + var response = request.Cookies!.Select(x => $"{x.Key}={x.Value}").ToArray(); + return WireMockTestServer.CreateJson(response); + } + + static ResponseMessage HandleSetCookies(IRequestMessage request) { + var cookies = new List { + new("cookie1", "value1", new()), + new("cookie2", "value2", new() { Path = "/path_extra" }), + new("cookie3", "value3", new() { Expires = DateTimeOffset.Now.AddDays(2) }), + new("cookie4", "value4", new() { MaxAge = TimeSpan.FromSeconds(100) }), + new("cookie5", "value5", new() { Secure = true }), + new("cookie6", "value6", new() { HttpOnly = true }), + new("cookie_empty_domain", "value_empty_domain", new() { HttpOnly = true, Domain = string.Empty }) + }; + + var response = new ResponseMessage { + Headers = new Dictionary>(), + BodyData = new BodyData { + DetectedBodyType = BodyType.String, + BodyAsString = "success" + } + }; + + var valuesList = new WireMockList(); + valuesList.AddRange(cookies.Select(cookie => cookie.Options.GetHeader(cookie.Name, cookie.Value))); + response.Headers.Add(KnownHeaders.SetCookie, valuesList); + + return response; + } + + static ResponseMessage HandleInvalidCookies(IRequestMessage request) { + var response = new ResponseMessage { + Headers = new Dictionary>(), + BodyData = new BodyData { + DetectedBodyType = BodyType.String, + BodyAsString = "success" + } + }; + + // Create an invalid cookie with a domain mismatch that will cause CookieException + // The cookie domain doesn't match the request URL domain + var valuesList = new WireMockList { "invalid_cookie=value; Domain=.invalid-domain.com" }; + response.Headers.Add(KnownHeaders.SetCookie, valuesList); + + return response; + } + + record CookieInternal(string Name, string Value, CookieOptions Options); + + public void Dispose() { + _client.Dispose(); + _server.Dispose(); + } +} + +static class CookieExtensions { + public static string GetHeader(this CookieOptions self, string name, string value) { + var cookieHeader = new SetCookieHeaderValue((StringSegment)name, (StringSegment)value) { + Domain = (StringSegment)self.Domain, + Path = (StringSegment)self.Path, + Expires = self.Expires, + Secure = self.Secure, + HttpOnly = self.HttpOnly, + MaxAge = self.MaxAge, + SameSite = (Microsoft.Net.Http.Headers.SameSiteMode)self.SameSite + }; + return cookieHeader.ToString(); + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/DefaultParameterTests.cs b/test/RestSharp.Tests.Integrated/DefaultParameterTests.cs new file mode 100644 index 000000000..f61c5a55d --- /dev/null +++ b/test/RestSharp.Tests.Integrated/DefaultParameterTests.cs @@ -0,0 +1,38 @@ +using RestSharp.Tests.Shared.Extensions; +using RestSharp.Tests.Shared.Fixtures; + +namespace RestSharp.Tests.Integrated; + +public sealed class DefaultParameterTests(WireMockTestServer server) : IClassFixture { + readonly RequestBodyCapturer _capturer = server.ConfigureBodyCapturer(Method.Get, false); + + [Fact] + public async Task Should_add_default_and_request_query_get_parameters() { + using var client = new RestClient(server.Url!).AddDefaultParameter("foo", "bar", ParameterType.QueryString); + var request = new RestRequest().AddParameter("foo1", "bar1", ParameterType.QueryString); + + await client.GetAsync(request); + + var query = _capturer.Url!.Query; + query.Should().Contain("foo=bar"); + query.Should().Contain("foo1=bar1"); + } + + [Fact] + public async Task Should_add_default_and_request_url_get_parameters() { + using var client = new RestClient($"{server.Url}/{{foo}}/").AddDefaultParameter("foo", "bar", ParameterType.UrlSegment); + var request = new RestRequest("{foo1}").AddParameter("foo1", "bar1", ParameterType.UrlSegment); + + await client.GetAsync(request); + + _capturer.Url!.Segments.Should().BeEquivalentTo("/", "bar/", "bar1"); + } + + [Fact] + public async Task Should_not_throw_exception_when_name_is_null() { + using var client = new RestClient($"{server.Url}/request-echo").AddDefaultParameter("foo", "bar", ParameterType.UrlSegment); + var request = new RestRequest("{foo1}").AddParameter(null, "value", ParameterType.RequestBody); + + await client.ExecuteAsync(request); + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/DownloadDataTests.cs b/test/RestSharp.Tests.Integrated/DownloadDataTests.cs new file mode 100644 index 000000000..2540b177f --- /dev/null +++ b/test/RestSharp.Tests.Integrated/DownloadDataTests.cs @@ -0,0 +1,79 @@ +using RestSharp.Extensions; + +namespace RestSharp.Tests.Integrated; + +public sealed class DownloadDataTests : IDisposable { + const string LocalPath = "Assets/Koala.jpg"; + + readonly WireMockServer _server = WireMockServer.Start(); + readonly RestClient _client; + readonly string _path = AppDomain.CurrentDomain.BaseDirectory; + + public DownloadDataTests() { + var pathToFile = Path.Combine(_path, Path.Combine(LocalPath.Split('/'))); + + _server + .Given(Request.Create().WithPath($"/{LocalPath}")) + .RespondWith(Response.Create().WithBodyFromFile(pathToFile)); + var options = new RestClientOptions($"{_server.Url}/{LocalPath}") { ThrowOnAnyError = true }; + _client = new(options); + } + + public void Dispose() => _server.Dispose(); + + [Fact] + public void DownloadDataAsync_returns_null_when_stream_is_null() { + var request = new RestRequest("/invalid-endpoint"); + + var action = () => _client.DownloadData(request); + + action.Should().ThrowExactly(); + } + + [Fact] + public async Task DownloadDataAsync_returns_bytes_when_stream_has_content() { + var request = new RestRequest(""); + + var bytes = await _client.DownloadDataAsync(request); + + bytes.Should().NotBeNull(); + bytes!.Length.Should().BeGreaterThan(0); + } + + [Fact] + public void DownloadData_sync_wraps_async() { + var request = new RestRequest(""); + + var bytes = _client.DownloadData(request); + + bytes.Should().NotBeNull(); + bytes!.Length.Should().BeGreaterThan(0); + } + + [Fact] + public void DownloadStream_sync_wraps_async() { + var request = new RestRequest(""); + + using var stream = _client.DownloadStream(request); + + stream.Should().NotBeNull(); + } + + [Fact] + public async Task DownloadStream_then_ReadAsBytes_matches_DownloadDataAsync() { + var request = new RestRequest(""); + +#if NET6_0_OR_GREATER + await using var stream = await _client.DownloadStreamAsync(request); +#else + using var stream = await _client.DownloadStreamAsync(request); +#endif + var bytesFromStream = stream == null ? null : await stream.ReadAsBytes(default); + + var bytesDirect = await _client.DownloadDataAsync(request); + + bytesFromStream.Should().NotBeNull(); + bytesDirect.Should().NotBeNull(); + bytesFromStream!.Should().BeEquivalentTo(bytesDirect); + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/DownloadFileTests.cs b/test/RestSharp.Tests.Integrated/DownloadFileTests.cs new file mode 100644 index 000000000..10fd2658a --- /dev/null +++ b/test/RestSharp.Tests.Integrated/DownloadFileTests.cs @@ -0,0 +1,86 @@ +using System.Text; + +// ReSharper disable MethodHasAsyncOverload + +namespace RestSharp.Tests.Integrated; + +public sealed class DownloadFileTests : IDisposable { + const string LocalPath = "Assets/Koala.jpg"; + + public DownloadFileTests() { + var pathToFile = Path.Combine(_path, Path.Combine(LocalPath.Split('/'))); + + _server + .Given(Request.Create().WithPath($"/{LocalPath}")) + .RespondWith(Response.Create().WithBodyFromFile(pathToFile)); + var options = new RestClientOptions($"{_server.Url}/{LocalPath}") { ThrowOnAnyError = true }; + _client = new(options); + } + + public void Dispose() => _server.Dispose(); + + readonly WireMockServer _server = WireMockServer.Start(); + readonly RestClient _client; + readonly string _path = AppDomain.CurrentDomain.BaseDirectory; + + [Fact] + public async Task AdvancedResponseWriter_without_ResponseWriter_reads_stream() { + var tag = string.Empty; + + var rr = new RestRequest("") { + AdvancedResponseWriter = (response, request) => { + var buf = new byte[16]; + // ReSharper disable once MustUseReturnValue + using var stream = response.Content.ReadAsStreamAsync().GetAwaiter().GetResult(); +#if NET + stream.ReadExactly(buf); +#else + stream.Read(buf, 0, buf.Length); +#endif + tag = Encoding.ASCII.GetString(buf, 6, 4); + return new RestResponse(request); + } + }; + + await _client.ExecuteAsync(rr); + Assert.Equal(0, string.Compare("JFIF", tag, StringComparison.Ordinal)); + } + + [Fact] + public async Task Handles_File_Download_Failure() { + var request = new RestRequest("some/other/path"); + var task = () => _client.DownloadDataAsync(request); + await task.Should().ThrowAsync().WithMessage("Request failed with status code NotFound"); + } + + [Fact] + public async Task Handles_Binary_File_Download() { + var request = new RestRequest(""); + var response = await _client.DownloadDataAsync(request); + var expected = File.ReadAllBytes(Path.Combine(_path, Path.Combine(LocalPath.Split('/')))); + + Assert.Equal(expected, response); + } + + [Fact] + public async Task Writes_Response_To_Stream() { + var tempFile = Path.GetTempFileName(); + + var request = new RestRequest("") { + ResponseWriter = responseStream => { + using var writer = File.OpenWrite(tempFile); + responseStream.CopyTo(writer); + return null; + } + }; + + var response = await _client.DownloadDataAsync(request); + + Assert.Null(response); + + var fromTemp = File.ReadAllBytes(tempFile); + var expected = File.ReadAllBytes(Path.Combine(_path, Path.Combine(LocalPath.Split('/')))); + + Assert.Equal(expected, fromTemp); + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/Fixtures/CaptureFixture.cs b/test/RestSharp.Tests.Integrated/Fixtures/CaptureFixture.cs new file mode 100644 index 000000000..2ca70efb7 --- /dev/null +++ b/test/RestSharp.Tests.Integrated/Fixtures/CaptureFixture.cs @@ -0,0 +1,22 @@ +using System.Collections.Specialized; + +namespace RestSharp.Tests.Integrated.Fixtures; + +public class CaptureFixture { + protected CaptureFixture() => RequestHeadCapturer.Initialize(); + + protected class RequestHeadCapturer { + public const string Resource = "Capture"; + + public static NameValueCollection? CapturedHeaders { get; private set; } + + public static void Initialize() => CapturedHeaders = null; + + // ReSharper disable once UnusedMember.Global + public static void Capture(HttpListenerContext context) { + var request = context.Request; + + CapturedHeaders = request.Headers; + } + } +} diff --git a/test/RestSharp.Tests.Integrated/Fixtures/CookieExtensions.cs b/test/RestSharp.Tests.Integrated/Fixtures/CookieExtensions.cs new file mode 100644 index 000000000..5d508d68b --- /dev/null +++ b/test/RestSharp.Tests.Integrated/Fixtures/CookieExtensions.cs @@ -0,0 +1,13 @@ +namespace RestSharp.Tests.Integrated.Fixtures; + +static class CookieExtensions { + public static Cookie? Find(this CookieCollection? cookieCollection, string name) { + if (cookieCollection == null) return null; + for (var i = 0; i < cookieCollection.Count; i++) { + var cookie = cookieCollection[i]; + if (cookie.Name == name) return cookie; + } + + return null; + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/HttpClientTests.cs b/test/RestSharp.Tests.Integrated/HttpClientTests.cs new file mode 100644 index 000000000..1acf553c3 --- /dev/null +++ b/test/RestSharp.Tests.Integrated/HttpClientTests.cs @@ -0,0 +1,16 @@ +namespace RestSharp.Tests.Integrated; + +public sealed class HttpClientTests(WireMockTestServer server) : IClassFixture { + [Fact] + public async Task ShouldUseBaseAddress() { + using var httpClient = new HttpClient(); + httpClient.BaseAddress = new Uri(server.Url!); + using var client = new RestClient(httpClient); + + var request = new RestRequest("success"); + var response = await client.ExecuteAsync(request); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + response.Data!.Message.Should().Be("Works!"); + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/HttpHeadersTests.cs b/test/RestSharp.Tests.Integrated/HttpHeadersTests.cs new file mode 100644 index 000000000..559c6435a --- /dev/null +++ b/test/RestSharp.Tests.Integrated/HttpHeadersTests.cs @@ -0,0 +1,90 @@ +namespace RestSharp.Tests.Integrated; + +public sealed class HttpHeadersTests(WireMockTestServer server) : IClassFixture, IDisposable { + const string UserAgent = "RestSharp/test"; + + readonly RestClient _client = new(new RestClientOptions(server.Url!) { ThrowOnAnyError = true, UserAgent = UserAgent }); + + [Fact] + public async Task Ensure_headers_correctly_set_in_the_interceptor() { + const string headerName = "HeaderName"; + const string headerValue = "HeaderValue"; + + var request = new RestRequest("/headers") { + Interceptors = [new HeaderInterceptor(headerName, headerValue)] + }; + + var response = await _client.ExecuteAsync(request); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + var header = FindHeader(response, headerName); + header.Should().NotBeNull(); + header.Value.Should().Be(headerValue); + } + + [Fact, Obsolete("Obsolete")] + public async Task Ensure_headers_correctly_set_in_the_hook() { + const string headerName = "HeaderName"; + const string headerValue = "HeaderValue"; + + var request = new RestRequest("/headers") { + OnBeforeRequest = http => { + http.Headers.Add(headerName, headerValue); + return default; + } + }; + + var response = await _client.ExecuteAsync(request); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + response.Data.Should().NotBeNull(); + var header = FindHeader(response, headerName); + header.Should().NotBeNull(); + header.Value.Should().Be(headerValue); + } + + [Fact] + public async Task Should_use_both_default_and_request_headers() { + var defaultHeader = new Header("defName", "defValue"); + var requestHeader = new Header("reqName", "reqValue"); + + _client.AddDefaultHeader(defaultHeader.Name, defaultHeader.Value); + + var request = new RestRequest("/headers").AddHeader(requestHeader.Name, requestHeader.Value); + + var response = await _client.ExecuteAsync(request); + CheckHeader(response, defaultHeader); + CheckHeader(response, requestHeader); + } + + [Fact] + public async Task Should_sent_custom_UserAgent() { + var request = new RestRequest("/headers"); + var response = await _client.ExecuteAsync(request); + var h = FindHeader(response, "User-Agent"); + h.Should().NotBeNull(); + h.Value.Should().Be(UserAgent); + + response.GetHeaderValue("Server").Should().Be("Kestrel"); + } + + static void CheckHeader(RestResponse response, Header header) { + var h = FindHeader(response, header.Name); + h.Should().NotBeNull(); + h.Value.Should().Be(header.Value); + } + + static TestServerResponse FindHeader(RestResponse response, string headerName) + => response.Data!.First(x => x.Name == headerName); + + record Header(string Name, string Value); + + class HeaderInterceptor(string headerName, string headerValue) : Interceptors.Interceptor { + public override ValueTask BeforeHttpRequest(HttpRequestMessage requestMessage, CancellationToken cancellationToken) { + requestMessage.Headers.Add(headerName, headerValue); + return default; + } + } + + public void Dispose() => _client.Dispose(); +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/HttpTracer/HttpMessageParts.cs b/test/RestSharp.Tests.Integrated/HttpTracer/HttpMessageParts.cs new file mode 100644 index 000000000..89610bebf --- /dev/null +++ b/test/RestSharp.Tests.Integrated/HttpTracer/HttpMessageParts.cs @@ -0,0 +1,23 @@ + +namespace RestSharp.Tests.Integrated.HttpTracer; + +[Flags] +public enum HttpMessageParts { + Unspecified = 0, + RequestBody = 1 << 1, + RequestHeaders = 1 << 2, + ResponseBody = 1 << 3, + ResponseHeaders = 1 << 4, + RequestCookies = 1 << 5, + + RequestAll = RequestBody | RequestHeaders | RequestCookies, + ResponseAll = ResponseBody | ResponseHeaders, + All = ResponseAll | RequestAll +} + +[Flags] +public enum JsonFormatting { + None = 0, + IndentRequest = 1 << 0, + IndentResponse = 1 << 1 +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/HttpTracer/HttpTracerHandler.cs b/test/RestSharp.Tests.Integrated/HttpTracer/HttpTracerHandler.cs new file mode 100644 index 000000000..c652de86e --- /dev/null +++ b/test/RestSharp.Tests.Integrated/HttpTracer/HttpTracerHandler.cs @@ -0,0 +1,244 @@ +using System.Diagnostics; +using System.Net.Http.Headers; +using System.Text; + +namespace RestSharp.Tests.Integrated.HttpTracer; + +public sealed class HttpTracerHandler : DelegatingHandler { + static HttpMessageParts DefaultVerbosity => HttpMessageParts.All; + + static string DefaultDurationFormat => "Duration: {0:ss\\:fffffff}"; + + static string LogMessageIndicatorPrefix => MessageIndicator; + + static string LogMessageIndicatorSuffix => MessageIndicator; + + /// + /// Instance verbosity bitmask, setting the instance verbosity overrides + /// + HttpMessageParts Verbosity { + get => field == HttpMessageParts.Unspecified ? DefaultVerbosity : field; + init; + } + + JsonFormatting JsonFormatting => JsonFormatting.None; + + /// Constructs the with a custom and a custom + /// User defined + /// User defined + /// Instance verbosity bitmask, setting the instance verbosity overrides + public HttpTracerHandler(HttpMessageHandler? handler, IHttpTracerLogger logger, HttpMessageParts verbosity = HttpMessageParts.Unspecified) { + InnerHandler = handler ?? + new HttpClientHandler { + AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate + }; + _logger = logger; + Verbosity = verbosity; + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { + try { + await LogHttpRequest(request).ConfigureAwait(false); + + var stopwatch = new Stopwatch(); + stopwatch.Start(); + var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false); + stopwatch.Stop(); + + await LogHttpResponse(response, stopwatch.Elapsed).ConfigureAwait(false); + return response; + } + catch (Exception ex) { + LogHttpException(request.Method, request.RequestUri, ex); + throw; + } + } + + static string ProcessRequestUri(Uri? uri) => uri?.ToString() ?? string.Empty; + + static string ProcessResponseLogHeading(HttpStatusCode statusCode, bool isSuccessStatusCode) { + const string succeeded = "SUCCEEDED"; + const string failed = "FAILED"; + + string responseResult; + + if (statusCode == default) { + responseResult = failed; + } + else { + responseResult = isSuccessStatusCode + ? $"{succeeded}: {(int)statusCode} {statusCode}" + : $"{failed}: {(int)statusCode} {statusCode}"; + } + + return responseResult; + } + + static string ProcessRequestHeaders(HttpRequestHeaders requestHeaders) => $"{requestHeaders.ToString().TrimEnd().TrimEnd('}').TrimStart('{')}"; + + static string ProcessResponseHeaders(HttpResponseMessage? responseHeaders) => responseHeaders?.ToString() ?? string.Empty; + + static string ProcessCookieHeader(CookieContainer cookieContainer, Uri requestRequestUri) => cookieContainer.GetCookieHeader(requestRequestUri); + + static Task ProcessRequestBody(HttpContent? requestContent) => requestContent?.ReadAsStringAsync() ?? Task.FromResult(string.Empty); + + static Task ProcessResponseBody(HttpContent? responseContent) => responseContent?.ReadAsStringAsync() ?? Task.FromResult(string.Empty); + + async Task LogHttpRequest(HttpRequestMessage request) { + var sb = new StringBuilder(); + + ConditionalAddRequestPrefix(request.Method, request.RequestUri, sb); + ConditionalAddRequestHeaders(request.Headers, sb); + ConditionalAddCookies(request.RequestUri, sb); + await ConditionalAddRequestBody(request.Content, sb); + + if (sb.Length > 0) + _logger.Log(sb.ToString()); + } + + async Task LogHttpResponse(HttpResponseMessage? response, TimeSpan duration) { + var sb = new StringBuilder(); + + ConditionalAddResponsePrefix( + response?.StatusCode, + response?.IsSuccessStatusCode, + response?.RequestMessage?.Method, + response?.RequestMessage?.RequestUri, + sb + ); + ConditionalAddResponseHeaders(response, sb); + await ConditionalAddResponseBody(response?.Content, sb); + ConditionalAddResponsePostfix(duration, sb); + + if (sb.Length > 0) + _logger.Log(sb.ToString()); + } + + void LogHttpException(HttpMethod requestMethod, Uri? requestUri, Exception ex) { + var httpExceptionString = $""" + {LogMessageIndicatorPrefix} HTTP EXCEPTION: [{requestMethod}]{LogMessageIndicatorSuffix} + {requestMethod} {requestUri} + {ex} + """; + _logger.Log(httpExceptionString); + } + + private void ConditionalAddResponsePostfix(TimeSpan duration, StringBuilder sb) { + if (!Verbosity.HasFlag(HttpMessageParts.ResponseHeaders) && !Verbosity.HasFlag(HttpMessageParts.ResponseBody)) return; + + var httpResponsePostfix = string.Format(DefaultDurationFormat, duration); + sb.AppendLine(httpResponsePostfix); + } + + private async Task ConditionalAddResponseBody(HttpContent? responseContent, StringBuilder sb) { + if (!Verbosity.HasFlag(HttpMessageParts.ResponseBody)) return; + + var httpResponseContent = await ProcessResponseBody(responseContent).ConfigureAwait(false); + + if (JsonFormatting.HasFlag(JsonFormatting.IndentResponse) && responseContent?.Headers.ContentType?.MediaType == JsonContentType) { + httpResponseContent = PrettyFormatJson(httpResponseContent); + } + + sb.AppendLine(httpResponseContent); + } + + private void ConditionalAddResponseHeaders(HttpResponseMessage? responseHeaders, StringBuilder sb) { + if (!Verbosity.HasFlag(HttpMessageParts.ResponseHeaders)) return; + + var httpResponseHeaders = ProcessResponseHeaders(responseHeaders); + sb.AppendLine(httpResponseHeaders); + } + + private void ConditionalAddResponsePrefix( + HttpStatusCode? responseStatusCode, + bool? responseIsSuccessStatusCode, + HttpMethod? requestMessageMethod, + Uri? requestMessageRequestUri, + StringBuilder sb + ) { + if (!Verbosity.HasFlag(HttpMessageParts.ResponseHeaders) && !Verbosity.HasFlag(HttpMessageParts.ResponseBody)) return; + + var responseResult = ProcessResponseLogHeading(responseStatusCode ?? default, responseIsSuccessStatusCode ?? false); + + var httpResponsePrefix = $"{LogMessageIndicatorPrefix}HTTP RESPONSE: [{responseResult}]{LogMessageIndicatorSuffix}"; + sb.AppendLine(httpResponsePrefix); + + var httpRequestMethodUri = $"{requestMessageMethod} {ProcessRequestUri(requestMessageRequestUri)}"; + sb.AppendLine(httpRequestMethodUri); + } + + private async Task ConditionalAddRequestBody(HttpContent? requestContent, StringBuilder sb) { + if (!Verbosity.HasFlag(HttpMessageParts.RequestBody)) return; + + var httpRequestBody = await ProcessRequestBody(requestContent).ConfigureAwait(false); + + if (JsonFormatting.HasFlag(JsonFormatting.IndentRequest) && requestContent?.Headers.ContentType?.MediaType == JsonContentType) { + httpRequestBody = PrettyFormatJson(httpRequestBody); + } + + sb.AppendLine(httpRequestBody); + } + + private void ConditionalAddRequestHeaders(HttpRequestHeaders requestHeaders, StringBuilder sb) { + if (!Verbosity.HasFlag(HttpMessageParts.RequestHeaders)) return; + + var httpErrorRequestHeaders = ProcessRequestHeaders(requestHeaders); + sb.AppendLine(httpErrorRequestHeaders); + } + + private void ConditionalAddRequestPrefix(HttpMethod requestMethod, Uri? requestUri, StringBuilder sb) { + if (!Verbosity.HasFlag(HttpMessageParts.RequestHeaders) && !Verbosity.HasFlag(HttpMessageParts.RequestBody)) return; + + var httpRequestPrefix = $"{LogMessageIndicatorPrefix}HTTP REQUEST: [{requestMethod}]{LogMessageIndicatorSuffix}"; + sb.AppendLine(httpRequestPrefix); + + var httpRequestMethodUri = $"{requestMethod} {ProcessRequestUri(requestUri)}"; + sb.AppendLine(httpRequestMethodUri); + } + + private void ConditionalAddCookies(Uri? requestUri, StringBuilder sb) { + if (!Verbosity.HasFlag(HttpMessageParts.RequestCookies) || + InnerHandler is not HttpClientHandler httpClientHandler) return; + + var cookieHeader = ProcessCookieHeader(httpClientHandler.CookieContainer, requestUri ?? new Uri("")); + if (string.IsNullOrWhiteSpace(cookieHeader)) return; + + sb.AppendLine($"{Environment.NewLine}Cookie: {cookieHeader}"); + } + + private static string PrettyFormatJson(string json) { + var indentation = 0; + var quoteCount = 0; + + var result = json.Select(ch => new { ch, quotes = ch == '"' ? quoteCount++ : quoteCount }) + .Select(t => new { + t, + lineBreak = t.ch == ',' && t.quotes % 2 == 0 + ? $"{t.ch}{Environment.NewLine}{string.Concat(Enumerable.Repeat(JsonIndentationString, indentation))}" + : null + } + ) + .Select(t => new { + t, + openChar = t.t.ch is '{' or '[' + ? $"{t.t.ch}{Environment.NewLine}{string.Concat(Enumerable.Repeat(JsonIndentationString, ++indentation))}" + : t.t.ch.ToString() + } + ) + .Select(t => new { + t, + closeChar = t.t.t.ch is '}' or ']' + ? $"{Environment.NewLine}{string.Concat(Enumerable.Repeat(JsonIndentationString, --indentation))}{t.t.t.ch}" + : t.t.t.ch.ToString() + } + ) + .Select(t => t.t.t.lineBreak ?? (t.t.openChar.Length > 1 ? t.t.openChar : t.closeChar)); + + return string.Concat(result); + } + + private readonly IHttpTracerLogger _logger; + private const string JsonContentType = "application/json"; + private const string MessageIndicator = " ==================== "; + private const string JsonIndentationString = " "; +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/HttpTracer/IHttpTracerLogger.cs b/test/RestSharp.Tests.Integrated/HttpTracer/IHttpTracerLogger.cs new file mode 100644 index 000000000..e19172655 --- /dev/null +++ b/test/RestSharp.Tests.Integrated/HttpTracer/IHttpTracerLogger.cs @@ -0,0 +1,9 @@ +namespace RestSharp.Tests.Integrated.HttpTracer; + +public interface IHttpTracerLogger { + void Log(string message); +} + +public class OutputHttpTracerLogger(ITestOutputHelper output) : IHttpTracerLogger { + public void Log(string message) => output.WriteLine(message); +} diff --git a/test/RestSharp.Tests.Integrated/Interceptor/InterceptorTests.cs b/test/RestSharp.Tests.Integrated/Interceptor/InterceptorTests.cs new file mode 100644 index 000000000..d52577217 --- /dev/null +++ b/test/RestSharp.Tests.Integrated/Interceptor/InterceptorTests.cs @@ -0,0 +1,166 @@ +// ReSharper disable AccessToDisposedClosure + +namespace RestSharp.Tests.Integrated.Interceptor; + +public class InterceptorTests(WireMockTestServer server) : IClassFixture { + [Fact] + public async Task Should_call_client_interceptor() { + // Arrange + var request = CreateRequest(); + + var (client, interceptor) = SetupClient( + test => test.BeforeRequestAction = req => req.AddHeader("foo", "bar") + ); + + //Act + var response = await client.ExecutePostAsync(request); + + //Assert + response.Request.Parameters.Should().Contain(x => x.Name == "foo" && (string)x.Value! == "bar"); + interceptor.BeforeRequestCalled.Should().BeTrue(); + interceptor.BeforeHttpRequestCalled.Should().BeTrue(); + interceptor.AfterHttpRequestCalled.Should().BeTrue(); + interceptor.AfterRequestCalled.Should().BeTrue(); + interceptor.BeforeDeserializationCalled.Should().BeTrue(); + + client.Dispose(); + } + + [Fact] + public async Task Should_call_request_interceptor() { + // Arrange + var request = CreateRequest(); + var interceptor = new TestInterceptor(); + request.Interceptors = new List { interceptor }; + + //Act + using var client = new RestClient(server.Url!); + await client.ExecutePostAsync(request); + + //Assert + interceptor.ShouldHaveCalledAll(); + } + + [Fact] + public async Task Should_call_both_client_and_request_interceptors() { + // Arrange + var request = CreateRequest(); + var (client, interceptor) = SetupClient(); + var requestInterceptor = new TestInterceptor(); + request.Interceptors = new List { requestInterceptor }; + + //Act + await client.ExecutePostAsync(request); + + //Assert + interceptor.ShouldHaveCalledAll(); + requestInterceptor.ShouldHaveCalledAll(); + + client.Dispose(); + } + + [Fact] + public async Task ThrowExceptionIn_InterceptBeforeRequest() { + //Arrange + var request = CreateRequest(); + var (client, interceptor) = SetupClient(test => test.BeforeRequestAction = req => throw new Exception("DummyException")); + + //Act + var action = () => client.ExecutePostAsync(request); + + //Assert + await action.Should().ThrowAsync().WithMessage("DummyException"); + interceptor.BeforeRequestCalled.Should().BeTrue(); + interceptor.BeforeHttpRequestCalled.Should().BeFalse(); + interceptor.AfterHttpRequestCalled.Should().BeFalse(); + interceptor.AfterRequestCalled.Should().BeFalse(); + interceptor.BeforeDeserializationCalled.Should().BeFalse(); + + client.Dispose(); + } + + [Fact] + public async Task ThrowExceptionIn_InterceptBeforeHttpRequest() { + // Arrange + var request = CreateRequest(); + var (client, interceptor) = SetupClient(test => test.BeforeHttpRequestAction = req => throw new Exception("DummyException")); + + //Act + var action = () => client.ExecutePostAsync(request); + + //Assert + await action.Should().ThrowAsync().WithMessage("DummyException"); + interceptor.BeforeRequestCalled.Should().BeTrue(); + interceptor.BeforeHttpRequestCalled.Should().BeTrue(); + interceptor.AfterHttpRequestCalled.Should().BeFalse(); + interceptor.AfterRequestCalled.Should().BeFalse(); + interceptor.BeforeDeserializationCalled.Should().BeFalse(); + + client.Dispose(); + } + + [Fact] + public async Task ThrowException_InInterceptAfterHttpRequest() { + // Arrange + var request = CreateRequest(); + var (client, interceptor) = SetupClient(test => test.AfterHttpRequestAction = req => throw new Exception("DummyException")); + + //Act + var action = () => client.ExecutePostAsync(request); + + //Assert + await action.Should().ThrowAsync().WithMessage("DummyException"); + interceptor.BeforeRequestCalled.Should().BeTrue(); + interceptor.BeforeHttpRequestCalled.Should().BeTrue(); + interceptor.AfterHttpRequestCalled.Should().BeTrue(); + interceptor.AfterRequestCalled.Should().BeFalse(); + interceptor.BeforeDeserializationCalled.Should().BeFalse(); + + client.Dispose(); + } + + [Fact] + public async Task ThrowExceptionIn_InterceptAfterRequest() { + // Arrange + var request = CreateRequest(); + var (client, interceptor) = SetupClient(test => test.AfterRequestAction = req => throw new Exception("DummyException")); + + //Act + var action = () => client.ExecutePostAsync(request); + + //Assert + await action.Should().ThrowAsync().WithMessage("DummyException"); + interceptor.BeforeRequestCalled.Should().BeTrue(); + interceptor.BeforeHttpRequestCalled.Should().BeTrue(); + interceptor.AfterHttpRequestCalled.Should().BeTrue(); + interceptor.AfterRequestCalled.Should().BeTrue(); + interceptor.BeforeDeserializationCalled.Should().BeFalse(); + + client.Dispose(); + } + + (RestClient client, TestInterceptor interceptor) SetupClient(Action? configureInterceptor = null) { + var interceptor = new TestInterceptor(); + configureInterceptor?.Invoke(interceptor); + + var options = new RestClientOptions(server.Url!) { + Interceptors = [interceptor] + }; + return (new RestClient(options), interceptor); + } + + static RestRequest CreateRequest() { + var body = new TestRequest("foo", 100); + return new RestRequest("post/json").AddJsonBody(body); + } +} + +static class InterceptorChecks { + public static void ShouldHaveCalledAll(this TestInterceptor interceptor) { + interceptor.BeforeRequestCalled.Should().BeTrue(); + interceptor.BeforeHttpRequestCalled.Should().BeTrue(); + interceptor.AfterHttpRequestCalled.Should().BeTrue(); + interceptor.AfterRequestCalled.Should().BeTrue(); + interceptor.BeforeDeserializationCalled.Should().BeTrue(); + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/Interceptor/TestInterceptor.cs b/test/RestSharp.Tests.Integrated/Interceptor/TestInterceptor.cs new file mode 100644 index 000000000..c5c88f8f9 --- /dev/null +++ b/test/RestSharp.Tests.Integrated/Interceptor/TestInterceptor.cs @@ -0,0 +1,45 @@ +namespace RestSharp.Tests.Integrated.Interceptor; + +class TestInterceptor : Interceptors.Interceptor { + internal bool BeforeRequestCalled { get; private set; } + internal bool BeforeHttpRequestCalled { get; private set; } + internal bool AfterHttpRequestCalled { get; private set; } + internal bool AfterRequestCalled { get; private set; } + internal bool BeforeDeserializationCalled { get; private set; } + + internal Action? BeforeRequestAction { get; set; } + internal Action? BeforeHttpRequestAction { get; set; } + internal Action? AfterHttpRequestAction { get; set; } + internal Action? AfterRequestAction { get; set; } + internal Action? BeforeDeserializationAction { get; set; } + + public override ValueTask BeforeHttpRequest(HttpRequestMessage req, CancellationToken cancellationToken) { + BeforeHttpRequestCalled = true; + BeforeHttpRequestAction?.Invoke(req); + return base.BeforeHttpRequest(req, cancellationToken); + } + + public override ValueTask AfterHttpRequest(HttpResponseMessage responseMessage, CancellationToken cancellationToken) { + AfterHttpRequestCalled = true; + AfterHttpRequestAction?.Invoke(responseMessage); + return base.AfterHttpRequest(responseMessage, cancellationToken); + } + + public override ValueTask AfterRequest(RestResponse response, CancellationToken cancellationToken) { + AfterRequestCalled = true; + AfterRequestAction?.Invoke(response); + return base.AfterRequest(response, cancellationToken); + } + + public override ValueTask BeforeRequest(RestRequest request, CancellationToken cancellationToken) { + BeforeRequestCalled = true; + BeforeRequestAction?.Invoke(request); + return base.BeforeRequest(request, cancellationToken); + } + + public override ValueTask BeforeDeserialization(RestResponse response, CancellationToken cancellationToken) { + BeforeDeserializationCalled = true; + BeforeDeserializationAction?.Invoke(response); + return base.BeforeDeserialization(response, cancellationToken); + } +} diff --git a/test/RestSharp.Tests.Integrated/JsonBodyTests.cs b/test/RestSharp.Tests.Integrated/JsonBodyTests.cs new file mode 100644 index 000000000..993702b94 --- /dev/null +++ b/test/RestSharp.Tests.Integrated/JsonBodyTests.cs @@ -0,0 +1,59 @@ +using System.Text.Json; +using RestSharp.Tests.Shared.Extensions; +using RestSharp.Tests.Shared.Fixtures; + +namespace RestSharp.Tests.Integrated; + +public sealed class JsonBodyTests : IDisposable { + readonly WireMockServer _server = WireMockServer.Start(); + readonly RestClient _client; + + public JsonBodyTests() => _client = new RestClient(_server.Url!); + + [Fact] + public async Task Query_Parameters_With_Json_Body() { + var capturer = _server.ConfigureBodyCapturer(Method.Put); + + var request = new RestRequest(RequestBodyCapturer.Resource, Method.Put) + .AddJsonBody(new { displayName = "Display Name" }) + .AddQueryParameter("key", "value"); + + await _client.ExecuteAsync(request); + + capturer.ContentType.Should().Be("application/json; charset=utf-8"); + capturer.Body.Should().Be("{\"displayName\":\"Display Name\"}"); + capturer.Url.Should().Be($"{_server.Url}{RequestBodyCapturer.Resource}?key=value"); + } + + [Fact] + public async Task Add_JSON_body_JSON_string() { + const string payload = "{\"displayName\":\"Display Name\"}"; + + var capturer = _server.ConfigureBodyCapturer(Method.Post); + var request = new RestRequest(RequestBodyCapturer.Resource, Method.Post).AddJsonBody(payload); + + await _client.ExecuteAsync(request); + + capturer.ContentType.Should().Be("application/json; charset=utf-8"); + capturer.Body.Should().Be(payload); + } + + [Fact] + public async Task Add_JSON_body_string() { + var payload = $" \"requestBody\": {{ \"content\": {{ \"{ContentType.Json}\": {{ \"schema\": {{ \"type\": \"string\" }} }} }} }}"; + + var expected = JsonSerializer.Serialize(payload); + var capturer = _server.ConfigureBodyCapturer(Method.Post); + var request = new RestRequest(RequestBodyCapturer.Resource, Method.Post).AddJsonBody(payload, true); + + await _client.ExecuteAsync(request); + + capturer.ContentType.Should().Be("application/json; charset=utf-8"); + capturer.Body.Should().Be(expected); + } + + public void Dispose() { + _server.Dispose(); + _client.Dispose(); + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/MultipartFormDataTests.cs b/test/RestSharp.Tests.Integrated/MultipartFormDataTests.cs new file mode 100644 index 000000000..1d5ee5899 --- /dev/null +++ b/test/RestSharp.Tests.Integrated/MultipartFormDataTests.cs @@ -0,0 +1,234 @@ +using RestSharp.Tests.Integrated.HttpTracer; +using RestSharp.Tests.Shared.Extensions; +using RestSharp.Tests.Shared.Fixtures; + +namespace RestSharp.Tests.Integrated; + +public sealed class MultipartFormDataTests : IDisposable { + readonly ITestOutputHelper _output; + + public MultipartFormDataTests(ITestOutputHelper output) { + _output = output; + _server = WireMockServer.Start(); + + _capturer = _server.ConfigureBodyCapturer(Method.Post); + + var options = new RestClientOptions($"{_server.Url!}{RequestBodyCapturer.Resource}") { + ConfigureMessageHandler = handler => new HttpTracerHandler(handler, new OutputHttpTracerLogger(output), HttpMessageParts.All) + }; + _client = new RestClient(options); + } + + public void Dispose() { + _server.Dispose(); + _client.Dispose(); + } + + const string LineBreak = "\r\n"; + + const string CharsetString = "charset=utf-8"; + const string ContentTypeString = $"{KnownHeaders.ContentType}: text/plain; {CharsetString}"; + const string ContentDispositionString = $"{KnownHeaders.ContentDisposition}: form-data;"; + + const string Expected = + $"--{{0}}{LineBreak}{ContentTypeString}{LineBreak}{ContentDispositionString} name=foo{LineBreak}{LineBreak}bar{LineBreak}" + + $"--{{0}}{LineBreak}{ContentTypeString}{LineBreak}{ContentDispositionString} name=\"a name with spaces\"{LineBreak}{LineBreak}somedata{LineBreak}" + + $"--{{0}}--{LineBreak}"; + + const string ExpectedFileAndBodyRequestContent = + "--{0}" + + $"{LineBreak}{KnownHeaders.ContentType}: application/octet-stream" + + $"{LineBreak}{KnownHeaders.ContentDisposition}: form-data; name=\"fileName\"; filename=\"TestFile.txt\"" + + $"{LineBreak}{LineBreak}This is a test file for RestSharp.{LineBreak}" + + $"--{{0}}{LineBreak}{KnownHeaders.ContentType}: application/json; {CharsetString}" + + $"{LineBreak}{KnownHeaders.ContentDisposition}: form-data; name=controlName" + + $"{LineBreak}{LineBreak}test{LineBreak}" + + $"--{{0}}--{LineBreak}"; + + const string ExpectedDefaultMultipartContentType = "multipart/form-data; boundary=\"{0}\""; + + const string ExpectedCustomMultipartContentType = "multipart/vnd.resteasy+form-data; boundary=\"{0}\""; + + readonly WireMockServer _server; + readonly RestClient _client; + readonly RequestBodyCapturer _capturer; + + static void AddParameters(RestRequest request) { + request.AddParameter("foo", "bar"); + request.AddParameter("a name with spaces", "somedata"); + } + + [Fact] + public async Task AlwaysMultipartFormData_WithParameter_Execute() { + var request = new RestRequest("?json_route=/posts") { + AlwaysMultipartFormData = true, + Method = Method.Post + }; + + request.AddParameter("title", "test", ParameterType.RequestBody); + + var response = await _client.ExecuteAsync(request); + + Assert.Null(response.ErrorException); + } + + [Fact] + public async Task MultipartFormData_NoBoundaryQuotes() { + var request = new RestRequest("/", Method.Post) { AlwaysMultipartFormData = true }; + + AddParameters(request); + request.MultipartFormQuoteBoundary = false; + + await _client.ExecuteAsync(request); + + var expected = string.Format(Expected, request.FormBoundary); + + _capturer.Body.Should().Be(expected); + _capturer.ContentType.Should().Be($"multipart/form-data; boundary={request.FormBoundary}"); + } + + [Fact] + public async Task MultipartFormData() { + var request = new RestRequest("/", Method.Post) { AlwaysMultipartFormData = true }; + + AddParameters(request); + + var response = await _client.ExecuteAsync(request); + + var expected = string.Format(Expected, request.FormBoundary); + + _output.WriteLine($"Expected: {expected}"); + _output.WriteLine($"Actual: {response.Content}"); + + _capturer.Body.Should().Be(expected); + _capturer.ContentType.Should().Be($"multipart/form-data; boundary=\"{request.FormBoundary}\""); + } + + [Fact] + public async Task MultipartFormData_HasDefaultContentType() { + var request = new RestRequest("/", Method.Post); + + var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Assets", "TestFile.txt"); + request.AddFile("fileName", path); + + request.AddParameter(new BodyParameter("controlName", "test", ContentType.Json)); + + var response = await _client.ExecuteAsync(request); + + var boundary = request.FormBoundary; + + var expectedFileAndBodyRequestContent = string.Format(ExpectedFileAndBodyRequestContent, boundary); + var expectedDefaultMultipartContentType = string.Format(ExpectedDefaultMultipartContentType, boundary); + + _output.WriteLine($"Expected: {expectedFileAndBodyRequestContent}"); + _output.WriteLine($"Actual: {response.Content}"); + + _capturer.Body.Should().Be(expectedFileAndBodyRequestContent); + _capturer.ContentType.Should().Be(expectedDefaultMultipartContentType); + } + + [Fact] + public async Task MultipartFormData_WithCustomContentType() { + var request = new RestRequest("/", Method.Post); + + var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Assets", "TestFile.txt"); + + const string customContentType = "multipart/vnd.resteasy+form-data"; + request.AddHeader(KnownHeaders.ContentType, customContentType); + request.AddFile("fileName", path); + request.AddParameter(new BodyParameter("controlName", "test", ContentType.Json)); + + await _client.ExecuteAsync(request); + var boundary = request.FormBoundary; + + var expectedFileAndBodyRequestContent = string.Format(ExpectedFileAndBodyRequestContent, boundary); + var expectedCustomMultipartContentType = string.Format(ExpectedCustomMultipartContentType, boundary); + + _capturer.Body.Should().Be(expectedFileAndBodyRequestContent); + _capturer.ContentType.Should().Be(expectedCustomMultipartContentType); + } + + [Fact] + public async Task MultipartFormData_WithParameterAndFile_Async() { + var request = new RestRequest("/", Method.Post) { + AlwaysMultipartFormData = true + }; + + var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Assets", "TestFile.txt"); + request.AddFile("fileName", path); + + request.AddParameter(new BodyParameter("controlName", "test", ContentType.Json)); + + await _client.ExecuteAsync(request); + var boundary = request.FormBoundary; + + var expectedFileAndBodyRequestContent = string.Format(ExpectedFileAndBodyRequestContent, boundary); + + _capturer.Body.Should().Be(expectedFileAndBodyRequestContent); + } + + [Fact] + public async Task MultipartFormDataAsync() { + var request = new RestRequest("/", Method.Post) { AlwaysMultipartFormData = true }; + + AddParameters(request); + + await _client.ExecuteAsync(request); + + var boundary = request.FormBoundary; + var expected = string.Format(Expected, boundary); + + _capturer.Body.Should().Be(expected); + } + + [Fact] + public async Task MultipartFormData_Without_File_Creates_A_Valid_RequestBody() { + using var client = new RestClient(_server.Url!); + + var request = new RestRequest(RequestBodyCapturer.Resource, Method.Post) { + AlwaysMultipartFormData = true + }; + var capturer = _server.ConfigureBodyCapturer(Method.Post); + + const string bodyData = "abc123 foo bar baz BING!"; + const string multipartName = "mybody"; + + request.AddParameter(new BodyParameter(multipartName, bodyData, ContentType.Plain)); + + await client.ExecuteAsync(request); + + var expectedBody = new[] { + ContentTypeString, + $"{ContentDispositionString} name={multipartName}", + bodyData + }; + + var actual = capturer.Body!.Replace("\n", string.Empty).Split('\r'); + actual.Should().Contain(expectedBody); + } + + [Fact] + public async Task PostParameter_contentType_in_multipart_form() { + using var client = new RestClient(_server.Url!); + + var request = new RestRequest(RequestBodyCapturer.Resource, Method.Post) { + AlwaysMultipartFormData = true + }; + var capturer = _server.ConfigureBodyCapturer(Method.Post); + + const string parameterName = "Arequest"; + const string parameterValue = "{\"attributeFormat\":\"pdf\"}"; + + var parameter = new GetOrPostParameter(parameterName, parameterValue) { + ContentType = "application/json" + }; + request.AddParameter(parameter); + + await client.ExecuteAsync(request); + + var actual = capturer.Body!.Replace("\n", string.Empty).Split('\r'); + actual[1].Should().Be("Content-Type: application/json; charset=utf-8"); + actual[2].Should().Be($"Content-Disposition: form-data; name={parameterName}"); + actual[4].Should().Be(parameterValue); + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/NonProtocolExceptionHandlingTests.cs b/test/RestSharp.Tests.Integrated/NonProtocolExceptionHandlingTests.cs new file mode 100644 index 000000000..b11f1ba02 --- /dev/null +++ b/test/RestSharp.Tests.Integrated/NonProtocolExceptionHandlingTests.cs @@ -0,0 +1,68 @@ +namespace RestSharp.Tests.Integrated; + +public sealed class NonProtocolExceptionHandlingTests : IDisposable { + public NonProtocolExceptionHandlingTests() + => _server + .Given(Request.Create().WithPath("/timeout")) + .RespondWith(Response.Create().WithDelay(TimeSpan.FromSeconds(1))); + + // ReSharper disable once ClassNeverInstantiated.Local + class StupidClass { + // ReSharper disable once UnusedMember.Local + public string Property { get; set; } = null!; + } + + public void Dispose() => _server.Dispose(); + + readonly WireMockServer _server = WireMockServer.Start(); + +#if NET + [Fact] + public async Task Handles_HttpClient_Timeout_Error() { + using var client = new RestClient(new HttpClient { Timeout = TimeSpan.FromMilliseconds(500) }); + + var request = new RestRequest($"{_server.Url}/timeout"); + var response = await client.ExecuteAsync(request); + + response.ErrorException.Should().BeOfType(); + response.ResponseStatus.Should().Be(ResponseStatus.TimedOut, response.ErrorMessage); + } +#endif + + [Fact] + public async Task Handles_Server_Timeout_Error() { + using var client = new RestClient(_server.Url!); + + var request = new RestRequest("timeout") { Timeout = TimeSpan.FromMilliseconds(500) }; + var response = await client.ExecuteAsync(request); + + response.ErrorException.Should().BeOfType(); + response.ResponseStatus.Should().Be(ResponseStatus.TimedOut); + } + + [Fact] + public async Task Handles_Server_Timeout_Error_With_Deserializer() { + using var client = new RestClient(_server.Url!); + + var request = new RestRequest("timeout") { Timeout = TimeSpan.FromMilliseconds(500) }; + var response = await client.ExecuteAsync(request); + + response.Data.Should().BeNull(); + response.ErrorException.Should().BeOfType(); + response.ResponseStatus.Should().Be(ResponseStatus.TimedOut); + } + + [Fact] + public async Task Handles_Non_Existent_Domain() { + using var client = new RestClient("http://this.cannot.exist:8001"); + + var request = new RestRequest("/") { + RequestFormat = DataFormat.Json, + Method = Method.Get + }; + var response = await client.ExecuteAsync(request); + + response.ErrorException.Should().BeOfType(); + response.ResponseStatus.Should().Be(ResponseStatus.Error); + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/NtlmTests.cs b/test/RestSharp.Tests.Integrated/NtlmTests.cs new file mode 100644 index 000000000..3edb1c140 --- /dev/null +++ b/test/RestSharp.Tests.Integrated/NtlmTests.cs @@ -0,0 +1,65 @@ +using System.Runtime.InteropServices; +using RestSharp.Tests.Integrated.Fixtures; +using RestSharp.Tests.Shared.Fixtures; + +namespace RestSharp.Tests.Integrated; + +/// +/// These tests use NTML auth and don't work on Linux, at least not in GH Actions +/// +public class NtlmTests : CaptureFixture { + [Fact] + public async Task Does_Not_Pass_Default_Credentials_When_Server_Does_Not_Negotiate() { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) return; + + using var server = SimpleServer.Create(Handlers.Generic()); + using var client = new RestClient(new RestClientOptions(server.Url) { UseDefaultCredentials = true }); + + var request = new RestRequest(RequestHeadCapturer.Resource); + await client.ExecuteAsync(request); + + Assert.NotNull(RequestHeadCapturer.CapturedHeaders); + var keys = RequestHeadCapturer.CapturedHeaders.Keys.Cast().ToArray(); + + Assert.False( + keys.Contains(KnownHeaders.Authorization), + "Authorization header was present in HTTP request from client, even though server does not use the Negotiate scheme" + ); + } + + [Fact] + public async Task Does_Not_Pass_Default_Credentials_When_UseDefaultCredentials_Is_False() { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) return; + + using var server = SimpleServer.Create(Handlers.Generic(), AuthenticationSchemes.Negotiate); + using var client = new RestClient(new RestClientOptions(server.Url) { UseDefaultCredentials = false }); + + var request = new RestRequest(RequestHeadCapturer.Resource); + var response = await client.ExecuteAsync(request); + + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + Assert.Null(RequestHeadCapturer.CapturedHeaders); + } + + [Fact] + public async Task Passes_Default_Credentials_When_UseDefaultCredentials_Is_True() { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return; + + using var server = SimpleServer.Create(Handlers.Generic(), AuthenticationSchemes.Negotiate); + using var client = new RestClient(new RestClientOptions(server.Url) { UseDefaultCredentials = true }); + + var request = new RestRequest(RequestHeadCapturer.Resource); + var response = await client.ExecuteAsync(request); + + response.StatusCode.Should().BeOneOf(HttpStatusCode.OK, HttpStatusCode.Unauthorized); + RequestHeadCapturer.CapturedHeaders.Should().NotBeNull(); + + var keys = RequestHeadCapturer.CapturedHeaders!.Keys.Cast().ToArray(); + + keys.Should() + .Contain( + KnownHeaders.Authorization, + "Authorization header not present in HTTP request from client, even though UseDefaultCredentials = true" + ); + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/PostTests.cs b/test/RestSharp.Tests.Integrated/PostTests.cs new file mode 100644 index 000000000..68b2777f0 --- /dev/null +++ b/test/RestSharp.Tests.Integrated/PostTests.cs @@ -0,0 +1,75 @@ +namespace RestSharp.Tests.Integrated; + +public sealed class PostTests(WireMockTestServer server) : IClassFixture, IDisposable { + readonly RestClient _client = new(server.Url!); + + [Fact] + public async Task Should_post_json() { + var body = new TestRequest("foo", 100); + var request = new RestRequest("post/json").AddJsonBody(body); + var response = await _client.ExecutePostAsync(request); + + response.Data!.Message.Should().Be(body.Data); + } + + [Fact] + public async Task Should_post_json_with_PostAsync() { + var body = new TestRequest("foo", 100); + var request = new RestRequest("post/json").AddJsonBody(body); + var response = await _client.PostAsync(request); + + response!.Message.Should().Be(body.Data); + } + + [Fact] + public async Task Should_post_json_with_PostJsonAsync() { + var body = new TestRequest("foo", 100); + var response = await _client.PostJsonAsync("post/json", body); + + response!.Message.Should().Be(body.Data); + } + + [Fact] + public async Task Should_post_large_form_data() { + const int length = 1024 * 1024; + + var superLongString = new string('?', length); + var request = new RestRequest("post/form", Method.Post).AddParameter("big_string", superLongString); + var response = await _client.ExecuteAsync(request); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + response.Data!.Message.Should().Be($"Works! Length: {length}"); + } + + [Fact] + public async Task Should_post_both_default_and_request_parameters() { + var defParam = new PostParameter("default", "default"); + var reqParam = new PostParameter("request", "request"); + + _client.AddDefaultParameter(defParam.Name, defParam.Value); + + var request = new RestRequest("post/data") + .AddParameter(reqParam.Name, reqParam.Value); + + var response = await _client.ExecutePostAsync(request); + response.StatusCode.Should().Be(HttpStatusCode.OK); + + CheckResponse(defParam); + CheckResponse(reqParam); + return; + + void CheckResponse(PostParameter parameter) { + var p = response.Data!.FirstOrDefault(x => x.Name == parameter.Name); + p.Should().NotBeNull(); + p!.Value.Should().Be(parameter.Value); + } + } + + class Response { + public string Message { get; set; } = null!; + } + + record PostParameter(string Name, string Value); + + public void Dispose() => _client.Dispose(); +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/ProxyTests.cs b/test/RestSharp.Tests.Integrated/ProxyTests.cs new file mode 100644 index 000000000..5562fe3fd --- /dev/null +++ b/test/RestSharp.Tests.Integrated/ProxyTests.cs @@ -0,0 +1,15 @@ +namespace RestSharp.Tests.Integrated; + +public class ProxyTests { + [Fact] + public async Task Set_Invalid_Proxy_Fails() { + using var server = WireMockServer.Start(); + using var client = new RestClient(new RestClientOptions(server.Url!) { Proxy = new WebProxy("non_existent_proxy", false) }); + + var request = new RestRequest(); + var response = await client.ExecuteAsync(request); + + response.IsSuccessful.Should().BeFalse(); + response.ErrorException.Should().BeOfType(); + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/PutTests.cs b/test/RestSharp.Tests.Integrated/PutTests.cs new file mode 100644 index 000000000..70a784d07 --- /dev/null +++ b/test/RestSharp.Tests.Integrated/PutTests.cs @@ -0,0 +1,42 @@ +using System.Text.Json; + +namespace RestSharp.Tests.Integrated; + +public sealed class PutTests(WireMockTestServer server) : IClassFixture, IDisposable { + readonly RestClient _client = new(server.Url!); + + static readonly JsonSerializerOptions Options = new(JsonSerializerDefaults.Web); + + [Fact] + public async Task Should_put_json_body() { + var body = new TestRequest("foo", 100); + var request = new RestRequest("/content").AddJsonBody(body); + + var response = await _client.PutAsync(request); + + var expected = JsonSerializer.Serialize(body, Options); + response.Content.Should().Be(expected); + } + + [Fact] + public async Task Should_put_json_body_using_extension() { + var body = new TestRequest("foo", 100); + var response = await _client.PutJsonAsync("/content", body); + + response.Should().BeEquivalentTo(body); + } + + [Fact] + public async Task Can_Timeout_PUT_Async() { + var request = new RestRequest("/timeout", Method.Put).AddBody("Body_Content"); + + // Half the value of ResponseHandler.Timeout + request.Timeout = TimeSpan.FromMilliseconds(200); + + var response = await _client.ExecuteAsync(request); + + Assert.Equal(ResponseStatus.TimedOut, response.ResponseStatus); + } + + public void Dispose() => _client.Dispose(); +} diff --git a/test/RestSharp.Tests.Integrated/RedirectTests.cs b/test/RestSharp.Tests.Integrated/RedirectTests.cs new file mode 100644 index 000000000..ec33a84cc --- /dev/null +++ b/test/RestSharp.Tests.Integrated/RedirectTests.cs @@ -0,0 +1,18 @@ +namespace RestSharp.Tests.Integrated; + +public sealed class RedirectTests(WireMockTestServer server) : IClassFixture, IDisposable { + readonly RestClient _client = new(new RestClientOptions(server.Url!) { FollowRedirects = true }); + + [Fact] + public async Task Can_Perform_GET_Async_With_Redirect() { + const string val = "Works!"; + + var request = new RestRequest("redirect"); + var response = await _client.ExecuteAsync(request); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + response.Data!.Message.Should().Be(val); + } + + public void Dispose() => _client.Dispose(); +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/RequestBodyTests.cs b/test/RestSharp.Tests.Integrated/RequestBodyTests.cs new file mode 100644 index 000000000..a2636d5e0 --- /dev/null +++ b/test/RestSharp.Tests.Integrated/RequestBodyTests.cs @@ -0,0 +1,88 @@ +using RestSharp.Tests.Shared.Extensions; +using RestSharp.Tests.Shared.Fixtures; + +namespace RestSharp.Tests.Integrated; + +public sealed class RequestBodyTests : IDisposable { + // const string NewLine = "\r\n"; + + static readonly string ExpectedTextContentType = $"{ContentType.Plain}; charset=utf-8"; + static readonly string ExpectedTextContentTypeNoCharset = ContentType.Plain; + + readonly WireMockServer _server = WireMockServer.Start(s => s.AllowBodyForAllHttpMethods = true); + + async Task AssertBody(Method method, bool disableCharset = false) { +#if NET + var options = new RestClientOptions(_server.Url!) { DisableCharset = disableCharset }; + using var client = new RestClient(options); + var request = new RestRequest(RequestBodyCapturer.Resource, method); + var capturer = _server.ConfigureBodyCapturer(method); + + const string bodyData = "abc123 foo bar baz BING!"; + + request.AddBody(bodyData, ContentType.Plain); + + await client.ExecuteAsync(request); + + var expected = disableCharset ? ExpectedTextContentTypeNoCharset : ExpectedTextContentType; + AssertHasRequestBody(capturer, expected, bodyData); +#endif + } + + [Fact] + public Task Can_Be_Added_To_COPY_Request() => AssertBody(Method.Copy); + + [Fact] + public Task Can_Be_Added_To_DELETE_Request() => AssertBody(Method.Delete); + + [Fact] + public Task Can_Be_Added_To_OPTIONS_Request() => AssertBody(Method.Options); + + [Fact] + public Task Can_Be_Added_To_PATCH_Request() => AssertBody(Method.Patch); + + [Fact] + public Task Can_Be_Added_To_POST_Request_NoCharset() => AssertBody(Method.Post, true); + + [Fact] + public Task Can_Be_Added_To_POST_Request() => AssertBody(Method.Post); + + [Fact] + public Task Can_Be_Added_To_PUT_Request_NoCharset() => AssertBody(Method.Put, true); + + [Fact] + public Task Can_Be_Added_To_PUT_Request() => AssertBody(Method.Put); + + [Fact] + public async Task Can_Have_No_Body_Added_To_POST_Request() { + const Method httpMethod = Method.Post; + + using var client = new RestClient(_server.Url!); + var request = new RestRequest(RequestBodyCapturer.Resource, httpMethod); + var capturer = _server.ConfigureBodyCapturer(httpMethod); + + await client.ExecuteAsync(request); + + AssertHasNoRequestBody(capturer); + } + + [Fact] + public Task Can_Be_Added_To_GET_Request() => AssertBody(Method.Get); + + [Fact] + public Task Can_Be_Added_To_HEAD_Request() => AssertBody(Method.Head); + + static void AssertHasNoRequestBody(RequestBodyCapturer capturer) { + capturer.ContentType.Should().BeNull(); + capturer.HasBody.Should().BeFalse(); + capturer.Body.Should().BeNullOrEmpty(); + } + + static void AssertHasRequestBody(RequestBodyCapturer capturer, string contentType, string bodyData) { + capturer.ContentType.Should().Be(contentType); + capturer.HasBody.Should().BeTrue(); + capturer.Body.Should().Be(bodyData); + } + + public void Dispose() => _server.Dispose(); +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/RequestFailureTests.cs b/test/RestSharp.Tests.Integrated/RequestFailureTests.cs new file mode 100644 index 000000000..c74306db5 --- /dev/null +++ b/test/RestSharp.Tests.Integrated/RequestFailureTests.cs @@ -0,0 +1,83 @@ +// ReSharper disable ClassNeverInstantiated.Local + +namespace RestSharp.Tests.Integrated; + +public sealed class RequestFailureTests(WireMockTestServer server) : IClassFixture, IDisposable { + readonly RestClient _client = new(server.Url!); + + [Fact] + public async Task Handles_GET_Request_Errors() { + var request = new RestRequest("status?code=404"); + var response = await _client.ExecuteAsync(request); + + response.StatusCode.Should().Be(HttpStatusCode.NotFound); + } + + [Fact] + public async Task Handles_GET_Request_Errors_With_Response_Type() { + 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().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!) { SetErrorExceptionOnUnsuccessfulStatusCode = 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); + } + + [Fact] + 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(); + } + + [Fact] + public async Task GetAsync_throws_on_unsuccessful_call() { + var request = new RestRequest("status?code=500"); + + var task = () => _client.GetAsync(request); + await task.Should().ThrowExactlyAsync(); + } + + [Fact] + public async Task GetAsync_completes_on_404() { + var request = new RestRequest("status?code=404"); + + var response = await _client.GetAsync(request); + response.StatusCode.Should().Be(HttpStatusCode.NotFound); + response.ResponseStatus.Should().Be(ResponseStatus.Completed); + } + + [Fact] + public async Task GetAsync_generic_throws_on_unsuccessful_call() { + var request = new RestRequest("status?code=500"); + + var task = () => _client.GetAsync(request); + await task.Should().ThrowExactlyAsync(); + } + + [Fact] + public async Task GetAsync_returns_null_on_404() { + var request = new RestRequest("status?code=404"); + + var response = await _client.GetAsync(request); + response.Should().BeNull(); + } + + public void Dispose() => _client.Dispose(); +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/RequestTests.cs b/test/RestSharp.Tests.Integrated/RequestTests.cs new file mode 100644 index 000000000..ea845ec61 --- /dev/null +++ b/test/RestSharp.Tests.Integrated/RequestTests.cs @@ -0,0 +1,10 @@ +namespace RestSharp.Tests.Integrated; + +public sealed class RequestTests(WireMockTestServer server) + : RequestTestsBase(false), IClassFixture, IDisposable { + readonly RestClient _client = new(server.Url!); + + public void Dispose() => _client.Dispose(); + + protected override IRestClient GetClient() => _client; +} diff --git a/test/RestSharp.Tests.Integrated/ResourceStringParametersTests.cs b/test/RestSharp.Tests.Integrated/ResourceStringParametersTests.cs new file mode 100644 index 000000000..e046cf926 --- /dev/null +++ b/test/RestSharp.Tests.Integrated/ResourceStringParametersTests.cs @@ -0,0 +1,28 @@ +namespace RestSharp.Tests.Integrated; + +public sealed class ResourceStringParametersTests : IDisposable { + readonly WireMockServer _server = WireMockServer.Start(); + + public void Dispose() => _server.Dispose(); + + [Fact] + public async Task Should_keep_to_parameters_with_the_same_name() { + const string parameters = "?priority=Low&priority=Medium"; + + var url = ""; + _server + .Given(Request.Create()) + .RespondWith(Response.Create().WithCallback(req => { + url = req.Url; + return new ResponseMessage(); + })); + + using var client = new RestClient(_server.Url!); + var request = new RestRequest(parameters); + + await client.GetAsync(request); + + var query = new Uri(url).Query; + query.Should().Be(parameters); + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/RestSharp.Tests.Integrated.csproj b/test/RestSharp.Tests.Integrated/RestSharp.Tests.Integrated.csproj new file mode 100644 index 000000000..8018e61be --- /dev/null +++ b/test/RestSharp.Tests.Integrated/RestSharp.Tests.Integrated.csproj @@ -0,0 +1,31 @@ + + + enable + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/RootElementTests.cs b/test/RestSharp.Tests.Integrated/RootElementTests.cs new file mode 100644 index 000000000..81da44392 --- /dev/null +++ b/test/RestSharp.Tests.Integrated/RootElementTests.cs @@ -0,0 +1,32 @@ +using RestSharp.Serializers.Xml; + +namespace RestSharp.Tests.Integrated; + +public class RootElementTests { + [Fact] + public async Task Copy_RootElement_From_Request_To_IWithRootElement_Deserializer() { + using var server = WireMockServer.Start(); + + const string xmlBody = + """ + + + + Works! + + + """; + server + .Given(Request.Create().WithPath("/success")) + .RespondWith(Response.Create().WithBody(xmlBody).WithHeader(KnownHeaders.ContentType, ContentType.Xml)); + + using var client = new RestClient(server.Url!, configureSerialization: cfg => cfg.UseXmlSerializer()); + + var request = new RestRequest("success") { RootElement = "Success" }; + + var response = await client.ExecuteAsync(request); + + response.Data.Should().NotBeNull(); + response.Data!.Message.Should().Be("Works!"); + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/StatusCodeTests.cs b/test/RestSharp.Tests.Integrated/StatusCodeTests.cs new file mode 100644 index 000000000..bb821e574 --- /dev/null +++ b/test/RestSharp.Tests.Integrated/StatusCodeTests.cs @@ -0,0 +1,85 @@ +using RestSharp.Serializers.Xml; + +// ReSharper disable UnusedMember.Local +// ReSharper disable InconsistentNaming + +namespace RestSharp.Tests.Integrated; + +public sealed class StatusCodeTests : IDisposable { + public StatusCodeTests() { + _client = new RestClient(_server.Url!, configureSerialization: cfg => cfg.UseXmlSerializer()); + _server + .Given(Request.Create()) + .RespondWith(Response.Create().WithCallback(CreateResponse)); + return; + + ResponseMessage CreateResponse(IRequestMessage request) { + var url = new Uri(request.Url); + + return new ResponseMessage { + StatusCode = int.Parse(url.Segments.Last()) + }; + } + } + + public void Dispose() { + _server.Dispose(); + _client.Dispose(); + } + + readonly WireMockServer _server = WireMockServer.Start(); + readonly RestClient _client; + + [Fact] + public async Task Handles_GET_Request_404_Error() { + var request = new RestRequest("404"); + var response = await _client.ExecuteAsync(request); + + response.StatusCode.Should().Be(HttpStatusCode.NotFound); + } + + [Fact] + public async Task Reports_1xx_Status_Code_Success_Accurately() { + var request = new RestRequest("100"); + var response = await _client.ExecuteAsync(request); + + response.IsSuccessful.Should().BeFalse(); + response.IsSuccessStatusCode.Should().BeFalse(); + } + + [Fact] + public async Task Reports_2xx_Status_Code_Success_Accurately() { + var request = new RestRequest("204"); + var response = await _client.ExecuteAsync(request); + + response.IsSuccessful.Should().BeTrue(); + response.IsSuccessStatusCode.Should().BeTrue(); + } + + [Fact] + public async Task Reports_3xx_Status_Code_Success_Accurately() { + var request = new RestRequest("301"); + var response = await _client.ExecuteAsync(request); + + response.IsSuccessful.Should().BeFalse(); + response.IsSuccessStatusCode.Should().BeFalse(); + } + + [Fact] + public async Task Reports_4xx_Status_Code_Success_Accurately() { + var request = new RestRequest("404"); + var response = await _client.ExecuteAsync(request); + + response.IsSuccessful.Should().BeFalse(); + response.IsSuccessStatusCode.Should().BeFalse(); + } + + [Fact] + public async Task Reports_5xx_Status_Code_Success_Accurately() { + var request = new RestRequest("503"); + var response = await _client.ExecuteAsync(request); + + response.IsSuccessful.Should().BeFalse(); + response.IsSuccessStatusCode.Should().BeFalse(); + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/StructuredSyntaxSuffixTests.cs b/test/RestSharp.Tests.Integrated/StructuredSyntaxSuffixTests.cs new file mode 100644 index 000000000..255ce7058 --- /dev/null +++ b/test/RestSharp.Tests.Integrated/StructuredSyntaxSuffixTests.cs @@ -0,0 +1,100 @@ +using System.Text; +using RestSharp.Serializers.Xml; +using WireMock.Types; +using WireMock.Util; + +// ReSharper disable UnusedAutoPropertyAccessor.Local + +namespace RestSharp.Tests.Integrated; + +public sealed class StructuredSyntaxSuffixTests : IDisposable { + readonly WireMockServer _server; + + class Person { + public string Name { get; set; } = null!; + + public int Age { get; set; } + } + + const string XmlContent = "Bob50"; + const string JsonContent = """{ "name":"Bob", "age":50 }"""; + + public StructuredSyntaxSuffixTests() { + _server = WireMockServer.Start(); + _server.Given(Request.Create().WithPath("/").UsingGet()).RespondWith(Response.Create().WithCallback(Handle)); + return; + + static ResponseMessage Handle(IRequestMessage request) { + var response = new ResponseMessage { + Headers = new Dictionary> { + [KnownHeaders.ContentType] = new(request.Query!["ct"]) + }, + StatusCode = 200, + BodyData = new BodyData { + BodyAsString = request.Query["c"].First(), + Encoding = Encoding.UTF8, + DetectedBodyType = BodyType.String + } + }; + return response; + } + } + + public void Dispose() => _server.Dispose(); + + [Fact] + public async Task By_default_application_json_content_type_should_deserialize_as_JSON() { + using var client = new RestClient(_server.Url!); + + var request = new RestRequest() + .AddParameter("ct", ContentType.Json) + .AddParameter("c", JsonContent); + + var response = await client.ExecuteAsync(request); + + response.Data!.Name.Should().Be("Bob"); + response.Data.Age.Should().Be(50); + } + + [Fact] + public async Task By_default_content_types_with_JSON_structured_syntax_suffix_should_deserialize_as_JSON() { + using var client = new RestClient(_server.Url!); + + var request = new RestRequest() + .AddParameter("ct", "application/vnd.somebody.something+json") + .AddParameter("c", JsonContent); + + var response = await client.ExecuteAsync(request); + + response.Data!.Name.Should().Be("Bob"); + response.Data.Age.Should().Be(50); + } + + [Fact] + public async Task By_default_content_types_with_XML_structured_syntax_suffix_should_deserialize_as_XML() { + using var client = new RestClient(_server.Url!, configureSerialization: cfg => cfg.UseXmlSerializer()); + + var request = new RestRequest() + .AddParameter("ct", "application/vnd.somebody.something+xml") + .AddParameter("c", XmlContent); + + var response = await client.ExecuteAsync(request); + + response.Data!.Name.Should().Be("Bob"); + response.Data.Age.Should().Be(50); + } + + [Fact] + public async Task By_default_text_xml_content_type_should_deserialize_as_XML() { + using var client = new RestClient(_server.Url!, configureSerialization: cfg => cfg.UseXmlSerializer()); + + var request = new RestRequest() + .AddParameter("ct", "text/xml") + .AddParameter("c", XmlContent); + + var response = await client.ExecuteAsync(request); + + Assert.Equal("Bob", response.Data!.Name); + Assert.Equal(50, response.Data.Age); + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/UploadFileTests.cs b/test/RestSharp.Tests.Integrated/UploadFileTests.cs new file mode 100644 index 000000000..533208669 --- /dev/null +++ b/test/RestSharp.Tests.Integrated/UploadFileTests.cs @@ -0,0 +1,114 @@ +// ReSharper disable MethodHasAsyncOverload + +using HttpMultipartParser; +using RestSharp.Extensions; + +namespace RestSharp.Tests.Integrated; + +public sealed class UploadFileTests : IDisposable { + readonly ITestOutputHelper _output; + readonly RestClient _client; + readonly string _basePath = AppDomain.CurrentDomain.BaseDirectory; + readonly string _path; + readonly UploadResponse _expected; + readonly WireMockServer _server = WireMockServer.Start(); + + const string Filename = "Koala.jpg"; + + public UploadFileTests(ITestOutputHelper output) { + _output = output; + _client = new RestClient(new RestClientOptions(_server.Url!)); + _path = Path.Combine(_basePath, "Assets", Filename); + _expected = new UploadResponse(Filename, new FileInfo(_path).Length, true); + + _server + .Given(Request.Create().WithPath("/upload")) + .RespondWith(Response.Create().WithCallback(HandleUpload)); + } + + [Fact] + public async Task Should_upload_from_file() { + var request = new RestRequest("upload").AddFile("file", _path); + var response = await _client.ExecutePostAsync(request); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + + _output.WriteLine(response.Content); + response.Data.Should().BeEquivalentTo(_expected); + } + + [Fact] + public async Task Should_upload_from_bytes() { + var bytes = File.ReadAllBytes(_path); + + var request = new RestRequest("upload").AddFile("file", bytes, Filename); + var response = await _client.ExecutePostAsync(request); + + _output.WriteLine(response.Content); + response.Data.Should().BeEquivalentTo(_expected); + } + + [Fact] + public async Task Should_upload_from_stream() { + var request = new RestRequest("upload").AddFile("file", () => File.OpenRead(_path), Filename); + var response = await _client.ExecutePostAsync(request); + + _output.WriteLine(response.Content); + response.Data.Should().BeEquivalentTo(_expected); + } + + // This test fails because MultipartFormDataParser doesn't understand filename* + [Fact] + public async Task Should_upload_from_stream_non_ascii() { + const string nonAsciiFilename = "Präsentation_Export.zip"; + + var options = new FileParameterOptions { DisableFilenameEncoding = true, DisableFilenameStar = false }; + + var request = new RestRequest("upload") + .AddFile("file", () => File.OpenRead(_path), nonAsciiFilename, options: options) + .AddQueryParameter("checkFile", "false"); + var response = await _client.ExecutePostAsync(request); + + _output.WriteLine(response.Content); + response.Data.Should().BeEquivalentTo(new UploadResponse(nonAsciiFilename, new FileInfo(_path).Length, true)); + } + + static async Task HandleUpload(IRequestMessage request) { + var response = new ResponseMessage(); + + var checkFile = request.Query == null || + request.Query.Count == 0 || + request.Query.ContainsKey("checkFile") && bool.Parse(request.Query["checkFile"][0]); + + using var stream = new MemoryStream(request.BodyAsBytes!); + var form = await MultipartFormDataParser.ParseAsync(stream); + if (form.Files.Count == 0) return response; + + var fileSection = form.Files[0]; + var fileLength = fileSection.Data.Length; + var fileName = fileSection.FileName; + + // ReSharper disable once InvertIf + if (checkFile) { + var assetPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Assets"); + + try { + var expected = File.ReadAllBytes(Path.Combine(assetPath, fileName)); + fileSection.Data.Seek(0, SeekOrigin.Begin); + var received = await fileSection.Data.ReadAsBytes(default); + var equal = received.SequenceEqual(expected); + return WireMockTestServer.CreateJson(new UploadResponse(fileName, fileLength, equal)); + } + catch (Exception) { + return response; + } + } + + return WireMockTestServer.CreateJson(new UploadResponse(fileName, fileLength, true)); + } + + public void Dispose() { + _client.Dispose(); + _server.Dispose(); + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/XmlResponseTests.cs b/test/RestSharp.Tests.Integrated/XmlResponseTests.cs new file mode 100644 index 000000000..257f18ad6 --- /dev/null +++ b/test/RestSharp.Tests.Integrated/XmlResponseTests.cs @@ -0,0 +1,124 @@ +using RestSharp.Interceptors; +using RestSharp.Serializers.Xml; + +namespace RestSharp.Tests.Integrated; + +public sealed class XmlResponseTests : IDisposable { + public XmlResponseTests() { + _server = WireMockServer.Start(); + + _server + .Given(Request.Create().WithPath("/contenttype_odata")) + .RespondWith(Response.Create().WithCallback(ContentTypeOData)); + + _server + .Given(Request.Create().WithPath("/success")) + .RespondWith( + Response + .Create() + .WithHeader(KnownHeaders.ContentType, ContentType.Xml) + .WithBody( + """ + + + + Works! + + + """ + ) + ); + + _server + .Given(Request.Create().WithPath("/error")) + .RespondWith( + Response + .Create() + .WithStatusCode(400) + .WithHeader(KnownHeaders.ContentType, ContentType.Xml) + .WithBody( + """ + + + + Not found! + + + """ + ) + ); + + _client = new RestClient(_server.Url!, configureSerialization: cfg => cfg.UseXmlSerializer()); + } + + public void Dispose() => _server.Dispose(); + + readonly WireMockServer _server; + readonly RestClient _client; + + [Fact] + public async Task Handles_Default_Root_Element_On_No_Error() { + var request = new RestRequest("success") { + RootElement = "Success" + }; + + var interceptor = new CompatibilityInterceptor { + OnBeforeDeserialization = resp => { + if (resp.StatusCode == HttpStatusCode.NotFound) request.RootElement = "Error"; + } + }; + request.Interceptors = [interceptor]; + + var response = await _client.ExecuteAsync(request); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + response.Data!.Message.Should().Be("Works!"); + } + + [Fact] + public async Task Handles_Different_Root_Element_On_Http_Error() { + var request = new RestRequest("error") { + RootElement = "Success", + Interceptors = [ + new CompatibilityInterceptor { + OnBeforeDeserialization = resp => { + if (resp.StatusCode == HttpStatusCode.BadRequest) resp.RootElement = "Error"; + } + } + ] + }; + + var response = await _client.ExecuteAsync(request); + + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + response.Data!.Message.Should().Be("Not found!"); + } + + [Fact] + public async Task ContentType_Additional_Information() { + var request = new RestRequest("", Method.Post) { + RequestFormat = DataFormat.Json, + Resource = "contenttype_odata" + }; + request.AddBody("bodyadsodajjd"); + request.AddHeader("X-RequestDigest", "xrequestdigestasdasd"); + request.AddHeader(KnownHeaders.Accept, $"{ContentType.Json}; odata=verbose"); + request.AddHeader(KnownHeaders.ContentType, $"{ContentType.Json}; odata=verbose"); + + var response = await _client.ExecuteAsync(request); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + response.IsSuccessful.Should().BeTrue(); + response.IsSuccessStatusCode.Should().BeTrue(); + } + + static ResponseMessage ContentTypeOData(IRequestMessage request) { + var contentType = request.Headers![KnownHeaders.ContentType]; + var hasCorrectHeader = contentType.Contains($"{ContentType.Json}; odata=verbose"); + + var response = new ResponseMessage { + StatusCode = hasCorrectHeader ? 200 : 400 + }; + return response; + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Serializers.Csv/CsvHelperTests.cs b/test/RestSharp.Tests.Serializers.Csv/CsvHelperTests.cs new file mode 100644 index 000000000..bb3ac3df7 --- /dev/null +++ b/test/RestSharp.Tests.Serializers.Csv/CsvHelperTests.cs @@ -0,0 +1,126 @@ +using RestSharp.Serializers.CsvHelper; +using RestSharp.Serializers.Json; +using System.Globalization; + +namespace RestSharp.Tests.Serializers.Csv; + +public sealed class CsvHelperTests : IDisposable { + static readonly Fixture Fixture = new(); + + static CsvHelperTests() => Fixture.Customize(new TestObjectCustomization()); + + readonly WireMockServer _server = WireMockServer.Start(); + + void ConfigureResponse(object expected) { + var serializer = new CsvHelperSerializer(); + + _server + .Given(Request.Create().WithPath("/").UsingGet()) + .RespondWith(Response.Create().WithBody(serializer.Serialize(expected)!).WithHeader(KnownHeaders.ContentType, ContentType.Csv)); + } + + [Fact] + public async Task Use_CsvHelper_For_Response() { + var expected = Fixture.Create(); + ConfigureResponse(expected); + using var client = new RestClient(_server.Url!, configureSerialization: cfg => cfg.UseCsvHelper()); + + var actual = await client.GetAsync(new RestRequest()); + actual.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task Use_CsvHelper_For_Collection_Response() { + var expected = Fixture.CreateMany(); + + ConfigureResponse(expected); + using var client = new RestClient(_server.Url!, configureSerialization: cfg => cfg.UseCsvHelper()); + + var actual = await client.GetAsync>(new RestRequest()); + actual.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task Invalid_csv_request_body_should_fail() { + _server + .Given(Request.Create().WithPath("/").UsingGet()) + .RespondWith(Response.Create().WithBody("invalid csv").WithHeader(KnownHeaders.ContentType, ContentType.Csv)); + + using var client = new RestClient(_server.Url!, configureSerialization: cfg => cfg.UseCsvHelper()); + + var response = await client.ExecuteAsync(new()); + response.IsSuccessStatusCode.Should().BeTrue(); + response.IsSuccessful.Should().BeFalse(); + } + + [Fact] + public async Task Valid_csv_response_should_succeed() { + var item = Fixture.Create(); + ConfigureResponse(item); + + using var client = new RestClient(_server.Url!, configureSerialization: cfg => cfg.UseSystemTextJson()); + + var response = await client.ExecuteAsync(new()); + response.IsSuccessStatusCode.Should().BeTrue(); + response.IsSuccessful.Should().BeTrue(); + } + + [Fact] + public void SerializedObject_Should_Be() { + var serializer = new CsvHelperSerializer(new(CultureInfo.InvariantCulture) { NewLine = ";" }); + + var item = Fixture.Create(); + var actual = serializer.Serialize(item); + var expected = $"{TestObject.Titles};{item.ToString(CultureInfo.InvariantCulture)};"; + + actual.Should().Be(expected); + } + + [Fact] + public void SerializedCollection_Should_Be() { + var serializer = new CsvHelperSerializer(new(CultureInfo.InvariantCulture) { NewLine = ";" }); + + var items = new TestObject[] { + new() { + Int32Value = 32, + SingleValue = 16.5f, + StringValue = "hello", + TimeSpanValue = TimeSpan.FromMinutes(10), + DateTimeValue = new(2024, 1, 20) + }, + new() { + Int32Value = 65, + DecimalValue = 89.555m, + TimeSpanValue = TimeSpan.FromSeconds(61), + DateTimeValue = new(2022, 8, 19, 5, 15, 21) + }, + new() { + SingleValue = 80000, + DoubleValue = 20.00001, + StringValue = "String, with comma" + } + }; + string[] strings = [TestObject.Titles, .. items.Select(i => i.ToString(CultureInfo.InvariantCulture))]; + + var expected = $"{string.Join(";", strings)};"; + var actual = serializer.Serialize(items); + + actual.Should().Be(expected); + } + + public void Dispose() => _server?.Dispose(); +} + +class TestObjectCustomization : ICustomization { + public void Customize(IFixture fixture) + => fixture.Customize(o => o + .WithAutoProperties() + .With( + p => p.DateTimeValue, + () => { + var dt = fixture.Create(); + return new(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second); + } + ) + ); +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Serializers.Csv/RestSharp.Tests.Serializers.Csv.csproj b/test/RestSharp.Tests.Serializers.Csv/RestSharp.Tests.Serializers.Csv.csproj new file mode 100644 index 000000000..c5305e02b --- /dev/null +++ b/test/RestSharp.Tests.Serializers.Csv/RestSharp.Tests.Serializers.Csv.csproj @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/test/RestSharp.Tests.Serializers.Csv/TestObject.cs b/test/RestSharp.Tests.Serializers.Csv/TestObject.cs new file mode 100644 index 000000000..6844f7040 --- /dev/null +++ b/test/RestSharp.Tests.Serializers.Csv/TestObject.cs @@ -0,0 +1,21 @@ +using System.Globalization; + +namespace RestSharp.Tests.Serializers.Csv; + +class TestObject { + public string StringValue { get; set; } + public int Int32Value { get; set; } + public decimal DecimalValue { get; set; } + public double DoubleValue { get; set; } + public float SingleValue { get; set; } + public DateTime DateTimeValue { get; set; } + public TimeSpan TimeSpanValue { get; set; } + + public const string Titles = + $"{nameof(StringValue)},{nameof(Int32Value)},{nameof(DecimalValue)},{nameof(DoubleValue)},{nameof(SingleValue)},{nameof(DateTimeValue)},{nameof(TimeSpanValue)}"; + + public string ToString(CultureInfo c) { + var str = StringValue?.Contains(',') == true ? $"\"{StringValue}\"" : StringValue; + return $"{str},{Int32Value},{DecimalValue.ToString(c)},{DoubleValue.ToString(c)},{SingleValue.ToString(c)},{DateTimeValue.ToString(c)},{TimeSpanValue.ToString()}"; + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Serializers.Json/NewtonsoftJson/IntegratedSimpleTests.cs b/test/RestSharp.Tests.Serializers.Json/NewtonsoftJson/IntegratedSimpleTests.cs new file mode 100644 index 000000000..995834822 --- /dev/null +++ b/test/RestSharp.Tests.Serializers.Json/NewtonsoftJson/IntegratedSimpleTests.cs @@ -0,0 +1,68 @@ +using RestSharp.Serializers.NewtonsoftJson; +using RestSharp.Tests.Shared.Extensions; + +namespace RestSharp.Tests.Serializers.Json.NewtonsoftJson; + +public sealed class IntegratedSimpleTests : IDisposable { + static readonly Fixture Fixture = new(); + + readonly WireMockServer _server = WireMockServer.Start(); + readonly RestClient _client; + + public IntegratedSimpleTests() => _client = new(_server.Url!, configureSerialization: cfg => cfg.UseNewtonsoftJson()); + + [Fact] + public async Task Should_serialize_request() { + var capturer = _server.ConfigureBodyCapturer(Method.Post, false); + var serializer = new JsonNetSerializer(); + var testData = Fixture.Create(); + var request = new RestRequest().AddJsonBody(testData); + + await _client.PostAsync(request); + var actual = serializer.Deserialize(new(request) { Content = capturer.Body! }); + + actual.Should().BeEquivalentTo(testData); + } + + [Fact] + public async Task Should_deserialize_response() { + var expected = Fixture.Create(); + + _server + .Given(Request.Create().WithPath("/").UsingGet()) + .RespondWith(Response.Create().WithBodyAsJson(expected)); + + var actual = await _client.GetAsync(new RestRequest()); + actual.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task Invalid_json_body_request_should_fail() { + _server + .Given(Request.Create().WithPath("/").UsingGet()) + .RespondWith(Response.Create().WithBody("invalid json").WithHeader(KnownHeaders.ContentType, ContentType.Json)); + + + var response = await _client.ExecuteAsync(new()); + response.IsSuccessStatusCode.Should().BeTrue(); + response.IsSuccessful.Should().BeFalse(); + } + + [Fact] + public async Task Valid_json_response_should_succeed() { + var item = Fixture.Create(); + + _server + .Given(Request.Create().WithPath("/").UsingGet()) + .RespondWith(Response.Create().WithBodyAsJson(item)); + + var response = await _client.ExecuteAsync(new()); + response.IsSuccessStatusCode.Should().BeTrue(); + response.IsSuccessful.Should().BeTrue(); + } + + public void Dispose() { + _server?.Dispose(); + _client.Dispose(); + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Serializers.Json/NewtonsoftJson/IntegratedTests.cs b/test/RestSharp.Tests.Serializers.Json/NewtonsoftJson/IntegratedTests.cs new file mode 100644 index 000000000..82925da7d --- /dev/null +++ b/test/RestSharp.Tests.Serializers.Json/NewtonsoftJson/IntegratedTests.cs @@ -0,0 +1,48 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using RestSharp.Serializers.NewtonsoftJson; + +namespace RestSharp.Tests.Serializers.Json.NewtonsoftJson; + +public sealed class IntegratedTests : IDisposable { + static readonly Fixture Fixture = new(); + + readonly WireMockServer _server = WireMockServer.Start(); + + [Fact, Obsolete("Obsolete")] + public async Task Use_with_GetJsonAsync() { + var data = Fixture.Create(); + var serialized = JsonConvert.SerializeObject(data, JsonNetSerializer.DefaultSettings); + + _server + .Given(Request.Create().WithPath("/test").UsingGet()) + .RespondWith(Response.Create().WithBody(serialized).WithHeader(KnownHeaders.ContentType, ContentType.Json)); + + using var client = new RestClient(_server.Url!, configureSerialization: cfg => cfg.UseNewtonsoftJson()); + + var response = await client.GetAsync("/test"); + + response.Should().BeEquivalentTo(data); + } + + [Fact] + public async Task Use_with_GetJsonAsync_custom_settings() { + var settings = new JsonSerializerSettings { + ContractResolver = new DefaultContractResolver { NamingStrategy = new SnakeCaseNamingStrategy() } + }; + var data = Fixture.Create(); + var serialized = JsonConvert.SerializeObject(data, settings); + + _server + .Given(Request.Create().WithPath("/test").UsingGet()) + .RespondWith(Response.Create().WithBody(serialized).WithHeader(KnownHeaders.ContentType, ContentType.Json)); + + using var client = new RestClient(_server.Url!, configureSerialization: cfg => cfg.UseNewtonsoftJson(settings)); + + var response = await client.GetAsync("/test"); + + response.Should().BeEquivalentTo(data); + } + + public void Dispose() => _server?.Dispose(); +} diff --git a/test/RestSharp.Tests.Serializers.Json/NewtonsoftJson/SerializationTests.cs b/test/RestSharp.Tests.Serializers.Json/NewtonsoftJson/SerializationTests.cs new file mode 100644 index 000000000..f996b6a3b --- /dev/null +++ b/test/RestSharp.Tests.Serializers.Json/NewtonsoftJson/SerializationTests.cs @@ -0,0 +1,45 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using RestSharp.Serializers.NewtonsoftJson; + +namespace RestSharp.Tests.Serializers.Json.NewtonsoftJson; + +public class SerializationTests { + static readonly Fixture Fixture = new(); + + readonly JsonSerializerSettings _jsonSerializerSettings = new() { + ContractResolver = new DefaultContractResolver { + NamingStrategy = new CamelCaseNamingStrategy() + }, + Formatting = Formatting.None + }; + + [Fact] + public void Serialize_multiple_objects_within_one_thread() { + var serializer = new JsonNetSerializer(); + var dummy = Fixture.CreateMany().ToArray(); + var expectedSerializations = dummy.Select( + d => JsonConvert.SerializeObject(d, _jsonSerializerSettings) + ).ToList(); + var actualSerializations = dummy.Select(serializer.Serialize).ToList(); + actualSerializations.Should().BeEquivalentTo(expectedSerializations); + } + + [Fact] + public void Serialize_within_multiple_threads() { + var serializer = new JsonNetSerializer(); + + Parallel.For( + 0, + 100, + _ => { + var dummy = Fixture.Create(); + + var expectedSerialization = JsonConvert.SerializeObject(dummy, _jsonSerializerSettings); + var actualSerialization = serializer.Serialize(dummy); + + actualSerialization.Should().Be(expectedSerialization); + } + ); + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Serializers.Json/RestSharp.Tests.Serializers.Json.csproj b/test/RestSharp.Tests.Serializers.Json/RestSharp.Tests.Serializers.Json.csproj new file mode 100644 index 000000000..ef87290b5 --- /dev/null +++ b/test/RestSharp.Tests.Serializers.Json/RestSharp.Tests.Serializers.Json.csproj @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/test/RestSharp.Tests.Serializers.Json/SampleData.cs b/test/RestSharp.Tests.Serializers.Json/SampleData.cs new file mode 100644 index 000000000..171d4b2ac --- /dev/null +++ b/test/RestSharp.Tests.Serializers.Json/SampleData.cs @@ -0,0 +1,14 @@ +// ReSharper disable UnusedMember.Global +namespace RestSharp.Tests.Serializers.Json; + +public class TestClass { + public string SimpleString { get; set; } + public int SimpleInt { get; set; } + public List List { get; set; } + public Subclass Sub { get; set; } + + public class Subclass { + public string Thing { get; set; } + public int AnotherThing { get; set; } + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Serializers.Json/SystemTextJson/SystemTextJsonTests.cs b/test/RestSharp.Tests.Serializers.Json/SystemTextJson/SystemTextJsonTests.cs new file mode 100644 index 000000000..5e913b2d2 --- /dev/null +++ b/test/RestSharp.Tests.Serializers.Json/SystemTextJson/SystemTextJsonTests.cs @@ -0,0 +1,68 @@ +using RestSharp.Serializers.Json; +using RestSharp.Tests.Shared.Extensions; + +namespace RestSharp.Tests.Serializers.Json.SystemTextJson; + +public sealed class SystemTextJsonTests : IDisposable { + static readonly Fixture Fixture = new(); + + readonly WireMockServer _server = WireMockServer.Start(); + readonly RestClient _client; + + public SystemTextJsonTests() => _client = new(_server.Url!); + + [Fact] + public async Task Should_serialize_request() { + var serializer = new SystemTextJsonSerializer(); + var capturer = _server.ConfigureBodyCapturer(Method.Post, false); + + var testData = Fixture.Create(); + var request = new RestRequest().AddJsonBody(testData); + + await _client.PostAsync(request); + + var actual = serializer.Deserialize(new(request) { Content = capturer.Body }); + actual.Should().BeEquivalentTo(testData); + } + + [Fact] + public async Task Should_deserialize_response_with_GenericGet() { + var expected = Fixture.Create(); + + _server + .Given(Request.Create().WithPath("/").UsingGet()) + .RespondWith(Response.Create().WithBodyAsJson(expected)); + + var actual = await _client.GetAsync(new RestRequest()); + actual.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task Posting_invalid_json_should_fail() { + _server + .Given(Request.Create().WithPath("/").UsingGet()) + .RespondWith(Response.Create().WithBody("invalid json").WithHeader(KnownHeaders.ContentType, ContentType.Json)); + + var response = await _client.ExecuteAsync(new()); + response.IsSuccessStatusCode.Should().BeTrue(); + response.IsSuccessful.Should().BeFalse(); + } + + [Fact] + public async Task Receiving_valid_json_should_succeed() { + var item = Fixture.Create(); + + _server + .Given(Request.Create().WithPath("/").UsingGet()) + .RespondWith(Response.Create().WithBodyAsJson(item)); + + var response = await _client.ExecuteAsync(new()); + response.IsSuccessStatusCode.Should().BeTrue(); + response.IsSuccessful.Should().BeTrue(); + } + + public void Dispose() { + _server?.Dispose(); + _client.Dispose(); + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Serializers.Xml/NamespacedXmlTests.cs b/test/RestSharp.Tests.Serializers.Xml/NamespacedXmlTests.cs new file mode 100644 index 000000000..a0572b45d --- /dev/null +++ b/test/RestSharp.Tests.Serializers.Xml/NamespacedXmlTests.cs @@ -0,0 +1,327 @@ +using System.Xml.Linq; +using RestSharp.Serializers.Xml; +using RestSharp.Tests.Serializers.Xml.SampleClasses; +using RestSharp.Tests.Serializers.Xml.SampleClasses.DeserializeAsTest; + +namespace RestSharp.Tests.Serializers.Xml; + +public class NamespacedXmlTests { + const string GuidString = "AC1FC4BC-087A-4242-B8EE-C53EBE9887A5"; + + static string CreateListOfPrimitivesXml() { + var doc = new XDocument(); + var ns = XNamespace.Get("http://restsharp.org"); + var root = new XElement(ns + "artists"); + + root.Add(new XElement(ns + "artist", "first")); + root.Add(new XElement(ns + "artist", "second")); + doc.Add(root); + + return doc.ToString(); + } + + static string CreateUnderscoresXml() { + var doc = new XDocument(); + var ns = XNamespace.Get("http://restsharp.org"); + var root = new XElement(ns + "Person"); + + root.Add(new XElement(ns + "Name", "John Sheehan")); + root.Add(new XElement(ns + "Start_Date", new DateTime(2009, 9, 25, 0, 6, 1))); + root.Add(new XAttribute(ns + "Age", 28)); + root.Add(new XElement(ns + "Percent", 99.9999m)); + root.Add(new XElement(ns + "Big_Number", long.MaxValue)); + root.Add(new XAttribute(ns + "Is_Cool", false)); + root.Add(new XElement(ns + "Ignore", "dummy")); + root.Add(new XAttribute(ns + "Read_Only", "dummy")); + root.Add(new XAttribute(ns + "Unique_Id", new Guid(GuidString))); + root.Add(new XElement(ns + "Url", "http://example.com")); + root.Add(new XElement(ns + "Url_Path", "/foo/bar")); + + root.Add( + new XElement( + ns + "Best_Friend", + new XElement(ns + "Name", "The Fonz"), + new XAttribute(ns + "Since", 1952) + ) + ); + + var friends = new XElement(ns + "Friends"); + + for (var i = 0; i < 10; i++) { + friends.Add( + new XElement( + ns + "Friend", + new XElement(ns + "Name", "Friend" + i), + new XAttribute(ns + "Since", DateTime.Now.Year - i) + ) + ); + } + + root.Add(friends); + + var foes = new XElement(ns + "Foes"); + + foes.Add(new XAttribute(ns + "Team", "Yankees")); + + for (var i = 0; i < 5; i++) foes.Add(new XElement(ns + "Foe", new XElement(ns + "Nickname", $"Foe{i}"))); + + root.Add(foes); + doc.Add(root); + + return doc.ToString(); + } + + static string CreateElementsXml() { + var doc = new XDocument(); + var ns = XNamespace.Get("http://restsharp.org"); + var root = new XElement(ns + "Person"); + + root.Add(new XElement(ns + "Name", "John Sheehan")); + root.Add(new XElement(ns + "StartDate", new DateTime(2009, 9, 25, 0, 6, 1))); + root.Add(new XElement(ns + "Age", 28)); + root.Add(new XElement(ns + "Percent", 99.9999m)); + root.Add(new XElement(ns + "BigNumber", long.MaxValue)); + root.Add(new XElement(ns + "IsCool", false)); + root.Add(new XElement(ns + "Ignore", "dummy")); + root.Add(new XElement(ns + "ReadOnly", "dummy")); + root.Add(new XElement(ns + "UniqueId", new Guid(GuidString))); + root.Add(new XElement(ns + "Url", "http://example.com")); + root.Add(new XElement(ns + "UrlPath", "/foo/bar")); + + root.Add( + new XElement( + ns + "BestFriend", + new XElement(ns + "Name", "The Fonz"), + new XElement(ns + "Since", 1952) + ) + ); + + var friends = new XElement(ns + "Friends"); + + for (var i = 0; i < 10; i++) { + friends.Add( + new XElement( + ns + "Friend", + new XElement(ns + "Name", "Friend" + i), + new XElement(ns + "Since", DateTime.Now.Year - i) + ) + ); + } + + root.Add(friends); + + root.Add( + new XElement( + ns + "FavoriteBand", + new XElement(ns + "Name", "Goldfinger") + ) + ); + + doc.Add(root); + + return doc.ToString(); + } + + static string CreateAttributesXml() { + var doc = new XDocument(); + var ns = XNamespace.Get("http://restsharp.org"); + var root = new XElement(ns + "Person"); + + root.Add(new XAttribute(ns + "Name", "John Sheehan")); + root.Add(new XAttribute(ns + "StartDate", new DateTime(2009, 9, 25, 0, 6, 1))); + root.Add(new XAttribute(ns + "Age", 28)); + root.Add(new XAttribute(ns + "Percent", 99.9999m)); + root.Add(new XAttribute(ns + "BigNumber", long.MaxValue)); + root.Add(new XAttribute(ns + "IsCool", false)); + root.Add(new XAttribute(ns + "Ignore", "dummy")); + root.Add(new XAttribute(ns + "ReadOnly", "dummy")); + root.Add(new XAttribute(ns + "UniqueId", new Guid(GuidString))); + root.Add(new XAttribute(ns + "Url", "http://example.com")); + root.Add(new XAttribute(ns + "UrlPath", "/foo/bar")); + + root.Add( + new XElement( + ns + "BestFriend", + new XAttribute(ns + "Name", "The Fonz"), + new XAttribute(ns + "Since", 1952) + ) + ); + + doc.Add(root); + + return doc.ToString(); + } + + [Fact] + public void Can_Deserialize_Attribute_Using_Exact_Name_Defined_In_DeserializeAs_Attribute() { + const string @namespace = "http://restsharp.org"; + + var ns = XNamespace.Get(@namespace); + + var doc = new XDocument( + new XElement( + ns + "response", + new XAttribute(ns + "attribute-value", "711"), + "random value" + ) + ); + + var expected = new NodeWithAttributeAndValue { + AttributeValue = "711" + }; + + var xml = new XmlDeserializer { Namespace = @namespace }; + var output = xml.Deserialize(new RestResponse { Content = doc.ToString() }); + + Assert.Equal(expected.AttributeValue, output.AttributeValue); + } + + [Fact] + public void Can_Deserialize_Attributes_With_Namespace() { + var doc = CreateAttributesXml(); + var response = new RestResponse { Content = doc }; + var d = new XmlDeserializer { Namespace = "http://restsharp.org" }; + var p = d.Deserialize(response); + + Assert.Equal("John Sheehan", p.Name); + Assert.Equal(new(2009, 9, 25, 0, 6, 1), p.StartDate); + Assert.Equal(28, p.Age); + Assert.Equal(long.MaxValue, p.BigNumber); + Assert.Equal(99.9999m, p.Percent); + Assert.False(p.IsCool); + Assert.Equal(new(GuidString), p.UniqueId); + Assert.Equal(new("http://example.com", UriKind.RelativeOrAbsolute), p.Url); + Assert.Equal(new("/foo/bar", UriKind.RelativeOrAbsolute), p.UrlPath); + Assert.NotNull(p.BestFriend); + Assert.Equal("The Fonz", p.BestFriend.Name); + Assert.Equal(1952, p.BestFriend.Since); + } + + [Fact] + public void Can_Deserialize_Elements_With_Namespace() { + var doc = CreateElementsXml(); + var response = new RestResponse { Content = doc }; + var d = new XmlDeserializer { Namespace = "http://restsharp.org" }; + var p = d.Deserialize(response); + + Assert.Equal("John Sheehan", p.Name); + Assert.Equal(new(2009, 9, 25, 0, 6, 1), p.StartDate); + Assert.Equal(28, p.Age); + Assert.Equal(long.MaxValue, p.BigNumber); + Assert.Equal(99.9999m, p.Percent); + Assert.False(p.IsCool); + Assert.Equal(new(GuidString), p.UniqueId); + Assert.Equal(new("http://example.com", UriKind.RelativeOrAbsolute), p.Url); + Assert.Equal(new("/foo/bar", UriKind.RelativeOrAbsolute), p.UrlPath); + Assert.NotNull(p.Friends); + Assert.Equal(10, p.Friends.Count); + Assert.NotNull(p.BestFriend); + Assert.Equal("The Fonz", p.BestFriend.Name); + Assert.Equal(1952, p.BestFriend.Since); + } + + [Fact] + public void Can_Deserialize_Elements_With_Namespace_Autodetect_Namespace() { + var doc = CreateElementsXml(); + var response = new RestResponse { Content = doc }; + var d = new XmlDeserializer(); + var p = d.Deserialize(response); + + Assert.Equal("John Sheehan", p.Name); + Assert.Equal(new(2009, 9, 25, 0, 6, 1), p.StartDate); + Assert.Equal(28, p.Age); + Assert.Equal(long.MaxValue, p.BigNumber); + Assert.Equal(99.9999m, p.Percent); + Assert.False(p.IsCool); + Assert.Equal(new(GuidString), p.UniqueId); + Assert.Equal(new("http://example.com", UriKind.RelativeOrAbsolute), p.Url); + Assert.Equal(new("/foo/bar", UriKind.RelativeOrAbsolute), p.UrlPath); + Assert.NotNull(p.Friends); + Assert.Equal(10, p.Friends.Count); + Assert.NotNull(p.BestFriend); + Assert.Equal("The Fonz", p.BestFriend.Name); + Assert.Equal(1952, p.BestFriend.Since); + } + + [Fact] + public void Can_Deserialize_List_Of_Primitives_With_Namespace() { + var doc = CreateListOfPrimitivesXml(); + var response = new RestResponse { Content = doc }; + var d = new XmlDeserializer { Namespace = "http://restsharp.org" }; + var a = d.Deserialize>(response); + + Assert.Equal(2, a.Count); + Assert.Equal("first", a[0].Value); + Assert.Equal("second", a[1].Value); + } + + [Fact] + public void Can_Deserialize_Names_With_Underscores_With_Namespace() { + var doc = CreateUnderscoresXml(); + var response = new RestResponse { Content = doc }; + var d = new XmlDeserializer { Namespace = "http://restsharp.org" }; + var p = d.Deserialize(response); + + Assert.Equal("John Sheehan", p.Name); + Assert.Equal(new(2009, 9, 25, 0, 6, 1), p.StartDate); + Assert.Equal(28, p.Age); + Assert.Equal(long.MaxValue, p.BigNumber); + Assert.Equal(99.9999m, p.Percent); + Assert.False(p.IsCool); + Assert.Equal(new(GuidString), p.UniqueId); + Assert.Equal(new("http://example.com", UriKind.RelativeOrAbsolute), p.Url); + Assert.Equal(new("/foo/bar", UriKind.RelativeOrAbsolute), p.UrlPath); + Assert.NotNull(p.Friends); + Assert.Equal(10, p.Friends.Count); + Assert.NotNull(p.BestFriend); + Assert.Equal("The Fonz", p.BestFriend.Name); + Assert.Equal(1952, p.BestFriend.Since); + Assert.NotNull(p.Foes); + Assert.Equal(5, p.Foes.Count); + Assert.Equal("Yankees", p.Foes.Team); + } + + [Fact] + public void Can_Deserialize_Node_Using_Exact_Name_Defined_In_DeserializeAs_Attribute() { + const string @namespace = "http://restsharp.org"; + var ns = XNamespace.Get(@namespace); + + var doc = new XDocument( + new XElement( + ns + "response", + new XElement(ns + "node-value", "711") + ) + ); + + var expected = new SingleNode { + Node = "711" + }; + + var xml = new XmlDeserializer(); + var output = xml.Deserialize(new() { Content = doc.ToString() }); + + Assert.NotNull(output); + + Assert.Equal(expected.Node, output.Node); + } + + [Fact] + public void Ignore_Protected_Property_That_Exists_In_Data() { + var doc = CreateElementsXml(); + var response = new RestResponse { Content = doc }; + var d = new XmlDeserializer { Namespace = "http://restsharp.org" }; + var p = d.Deserialize(response); + + Assert.Null(p.IgnoreProxy); + } + + [Fact] + public void Ignore_ReadOnly_Property_That_Exists_In_Data() { + var doc = CreateElementsXml(); + var response = new RestResponse { Content = doc }; + var d = new XmlDeserializer { Namespace = "http://restsharp.org" }; + var p = d.Deserialize(response); + + Assert.Null(p.ReadOnlyProxy); + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Serializers.Xml/RestSharp.Tests.Serializers.Xml.csproj b/test/RestSharp.Tests.Serializers.Xml/RestSharp.Tests.Serializers.Xml.csproj new file mode 100644 index 000000000..10c42ccc4 --- /dev/null +++ b/test/RestSharp.Tests.Serializers.Xml/RestSharp.Tests.Serializers.Xml.csproj @@ -0,0 +1,27 @@ + + + disable + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/RestSharp.Tests.Serializers.Xml/SampleClasses/BearerToken.cs b/test/RestSharp.Tests.Serializers.Xml/SampleClasses/BearerToken.cs new file mode 100644 index 000000000..2e3faf202 --- /dev/null +++ b/test/RestSharp.Tests.Serializers.Xml/SampleClasses/BearerToken.cs @@ -0,0 +1,14 @@ +using RestSharp.Serializers; + +namespace RestSharp.Tests.Serializers.Xml.SampleClasses; + +public class BearerToken { + public string AccessToken { get; set; } + public string TokenType { get; set; } + public uint ExpiresIn { get; set; } + public string UserName { get; set; } + [DeserializeAs(Name = ".issued")] + public DateTimeOffset Issued { get; set; } + [DeserializeAs(Name = ".expires")] + public DateTimeOffset Expires { get; set; } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Serializers.Xml/SampleClasses/BooleanTest.cs b/test/RestSharp.Tests.Serializers.Xml/SampleClasses/BooleanTest.cs new file mode 100644 index 000000000..cecd53dae --- /dev/null +++ b/test/RestSharp.Tests.Serializers.Xml/SampleClasses/BooleanTest.cs @@ -0,0 +1,5 @@ +namespace RestSharp.Tests.Serializers.Xml.SampleClasses; + +public class BooleanTest { + public bool Value { get; set; } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Serializers.Xml/SampleClasses/ColorWithValue.cs b/test/RestSharp.Tests.Serializers.Xml/SampleClasses/ColorWithValue.cs new file mode 100644 index 000000000..5e873fe20 --- /dev/null +++ b/test/RestSharp.Tests.Serializers.Xml/SampleClasses/ColorWithValue.cs @@ -0,0 +1,9 @@ +using RestSharp.Serializers; + +namespace RestSharp.Tests.Serializers.Xml.SampleClasses; + +[DeserializeAs(Name = "Color")] +public class ColorWithValue { + public string Name { get; set; } + public int Value { get; set; } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Serializers.Xml/SampleClasses/DeserializeAsTest/misc.cs b/test/RestSharp.Tests.Serializers.Xml/SampleClasses/DeserializeAsTest/misc.cs new file mode 100644 index 000000000..b8a4b9dfd --- /dev/null +++ b/test/RestSharp.Tests.Serializers.Xml/SampleClasses/DeserializeAsTest/misc.cs @@ -0,0 +1,13 @@ +using RestSharp.Serializers; + +namespace RestSharp.Tests.Serializers.Xml.SampleClasses.DeserializeAsTest; + +public class NodeWithAttributeAndValue { + [DeserializeAs(Name = "attribute-value", Attribute = true)] + public string AttributeValue { get; set; } +} + +public class SingleNode { + [DeserializeAs(Name = "node-value")] + public string Node { get; set; } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Serializers.Xml/SampleClasses/EmployeeTracker.cs b/test/RestSharp.Tests.Serializers.Xml/SampleClasses/EmployeeTracker.cs new file mode 100644 index 000000000..a9dcb002e --- /dev/null +++ b/test/RestSharp.Tests.Serializers.Xml/SampleClasses/EmployeeTracker.cs @@ -0,0 +1,29 @@ +namespace RestSharp.Tests.Serializers.Xml.SampleClasses; + +public class EmployeeTracker { + /// + /// Key: Employee name. + /// Value: Messages sent to employee. + /// + public Dictionary> EmployeesMail { get; set; } + + /// + /// Key: Employee name. + /// Value: Hours worked this each week. + /// + public Dictionary>> EmployeesTime { get; set; } + + /// + /// Key: Employee name. + /// Value: Payments made to employee + /// + public Dictionary> EmployeesPay { get; set; } +} + +public class Payment { + public PaymentType Type { get; set; } + + public int Amount { get; set; } +} + +public enum PaymentType { Bonus, Monthly, BiWeekly } \ No newline at end of file diff --git a/test/RestSharp.Tests.Serializers.Xml/SampleClasses/EnumTest.cs b/test/RestSharp.Tests.Serializers.Xml/SampleClasses/EnumTest.cs new file mode 100644 index 000000000..37efb663b --- /dev/null +++ b/test/RestSharp.Tests.Serializers.Xml/SampleClasses/EnumTest.cs @@ -0,0 +1,35 @@ +namespace RestSharp.Tests.Serializers.Xml.SampleClasses; + +public enum ByteEnum : byte { EnumMin = 0, EnumMax = 255 } + +public enum SByteEnum : sbyte { EnumMin = -128, EnumMax = 127 } + +public enum ShortEnum : short { EnumMin = -32768, EnumMax = 32767 } + +public enum UShortEnum : ushort { EnumMin = 0, EnumMax = 65535 } + +public enum IntEnum { EnumMin = -2147483648, EnumMax = 2147483647 } + +public enum UIntEnum : uint { EnumMin = 0, EnumMax = 4294967295 } + +public enum LongEnum : long { EnumMin = -9223372036854775808, EnumMax = 9223372036854775807 } + +public enum ULongEnum : ulong { EnumMin = 0, EnumMax = 18446744073709551615 } + +public class JsonEnumTypesTestStructure { + public ByteEnum ByteEnumType { get; set; } + + public SByteEnum SByteEnumType { get; set; } + + public ShortEnum ShortEnumType { get; set; } + + public UShortEnum UShortEnumType { get; set; } + + public IntEnum IntEnumType { get; set; } + + public UIntEnum UIntEnumType { get; set; } + + public LongEnum LongEnumType { get; set; } + + public ULongEnum ULongEnumType { get; set; } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Serializers.Xml/SampleClasses/Goodreads.cs b/test/RestSharp.Tests.Serializers.Xml/SampleClasses/Goodreads.cs new file mode 100644 index 000000000..5a8785b2f --- /dev/null +++ b/test/RestSharp.Tests.Serializers.Xml/SampleClasses/Goodreads.cs @@ -0,0 +1,21 @@ +namespace RestSharp.Tests.Serializers.Xml.SampleClasses; + +public class GoodReadsReviewCollection { + public int Start { get; set; } + + public int End { get; set; } + + public int Total { get; set; } + + public List Reviews { get; set; } +} + +public class GoodReadsReview { + public string Id { get; set; } + + public GoodReadsBook Book { get; set; } +} + +public class GoodReadsBook { + public string Isbn { get; set; } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Serializers.Xml/SampleClasses/GoogleWeatherWithAttributes.cs b/test/RestSharp.Tests.Serializers.Xml/SampleClasses/GoogleWeatherWithAttributes.cs new file mode 100644 index 000000000..8117871bc --- /dev/null +++ b/test/RestSharp.Tests.Serializers.Xml/SampleClasses/GoogleWeatherWithAttributes.cs @@ -0,0 +1,67 @@ +using RestSharp.Serializers; + +namespace RestSharp.Tests.Serializers.Xml.SampleClasses; + +public class GoogleWeatherApi { + public string Version { get; set; } + + public GoogleWeather Weather { get; set; } +} + +public class GoogleWeather : List { + public string ModuleId { get; set; } + + public string TabId { get; set; } + + public string MobileRow { get; set; } + + public string MobileZipped { get; set; } + + public string Row { get; set; } + + public string Section { get; set; } + + [DeserializeAs(Name = "forecast_information")] + public ForecastInformation Forecast { get; set; } + + [DeserializeAs(Name = "current_conditions")] + public CurrentConditions Current { get; set; } +} + +public class GoogleDataElement { + public string Data { get; set; } +} + +public class ForecastInformation { + public GoogleDataElement City { get; set; } + + public GoogleDataElement PostalCode { get; set; } + + public GoogleDataElement ForecastDate { get; set; } + + public GoogleDataElement UnitSystem { get; set; } +} + +public class CurrentConditions { + public GoogleDataElement Condition { get; set; } + + public GoogleDataElement TempC { get; set; } + + public GoogleDataElement Humidity { get; set; } + + public GoogleDataElement Icon { get; set; } + + public GoogleDataElement WindCondition { get; set; } +} + +public class ForecastConditions { + public GoogleDataElement DayOfWeek { get; set; } + + public GoogleDataElement Condition { get; set; } + + public GoogleDataElement Low { get; set; } + + public GoogleDataElement High { get; set; } + + public GoogleDataElement Icon { get; set; } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Serializers.Xml/SampleClasses/HeaderAndRows.cs b/test/RestSharp.Tests.Serializers.Xml/SampleClasses/HeaderAndRows.cs new file mode 100644 index 000000000..d0a7369f8 --- /dev/null +++ b/test/RestSharp.Tests.Serializers.Xml/SampleClasses/HeaderAndRows.cs @@ -0,0 +1,17 @@ +using RestSharp.Serializers; + +namespace RestSharp.Tests.Serializers.Xml.SampleClasses; + +public class Header { + public string Title { get; set; } + public string Body { get; set; } + public string Date { get; set; } + + [DeserializeAs(Name = "rows")] + public List Othername { get; set; } +} + +public class Row { + public string Text1 { get; set; } + public string Text2 { get; set; } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Serializers.Xml/SampleClasses/JsonLists.cs b/test/RestSharp.Tests.Serializers.Xml/SampleClasses/JsonLists.cs new file mode 100644 index 000000000..f94fb130c --- /dev/null +++ b/test/RestSharp.Tests.Serializers.Xml/SampleClasses/JsonLists.cs @@ -0,0 +1,7 @@ +namespace RestSharp.Tests.Serializers.Xml.SampleClasses; + +public class JsonLists { + public List Names { get; set; } + + public List Numbers { get; set; } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Serializers.Xml/SampleClasses/Lastfm.cs b/test/RestSharp.Tests.Serializers.Xml/SampleClasses/Lastfm.cs new file mode 100644 index 000000000..ac5174c3b --- /dev/null +++ b/test/RestSharp.Tests.Serializers.Xml/SampleClasses/Lastfm.cs @@ -0,0 +1,50 @@ +// ReSharper disable InconsistentNaming +// ReSharper disable UnusedMember.Global +// ReSharper disable UnusedAutoPropertyAccessor.Global +// ReSharper disable ClassNeverInstantiated.Global +#pragma warning disable CS8981 +namespace RestSharp.Tests.Serializers.Xml.SampleClasses; + +public class Event : LastfmBase { + public string id { get; set; } + + public string title { get; set; } + + public EventArtistList artists { get; set; } + + public Venue venue { get; set; } + + public DateTime startDate { get; set; } + + public string description { get; set; } + + public int attendance { get; set; } + + public int reviews { get; set; } + + public string tag { get; set; } + + public string url { get; set; } + + public string headliner { get; set; } + + public int cancelled { get; set; } +} + +public class EventArtistList : List; + +public class artist { + public string Value { get; set; } +} + +public abstract class LastfmBase { + public string status { get; set; } + + public Error error { get; set; } +} + +public class Error { + public string Value { get; set; } + + public int code { get; set; } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Serializers.Xml/SampleClasses/ListSamples.cs b/test/RestSharp.Tests.Serializers.Xml/SampleClasses/ListSamples.cs new file mode 100644 index 000000000..d8b1491e9 --- /dev/null +++ b/test/RestSharp.Tests.Serializers.Xml/SampleClasses/ListSamples.cs @@ -0,0 +1,41 @@ +// ReSharper disable InconsistentNaming +#pragma warning disable CS8981 +namespace RestSharp.Tests.Serializers.Xml.SampleClasses; + +public class SimpleTypesListSample { + public List Names { get; set; } + + public List Numbers { get; set; } +} + +public class InlineListSample { + public int Count { get; set; } + + public List images { get; set; } + + public List Images { get; set; } +} + +public class NestedListSample { + public List images { get; set; } + + public List Images { get; set; } +} + +public class EmptyListSample { + public List images { get; set; } + + public List Images { get; set; } +} + +public class Image { + public string Src { get; set; } + + public string Value { get; set; } +} + +public class image { + public string Src { get; set; } + + public string Value { get; set; } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Serializers.Xml/SampleClasses/NestedElementTestClasses.cs b/test/RestSharp.Tests.Serializers.Xml/SampleClasses/NestedElementTestClasses.cs new file mode 100644 index 000000000..d6a512c44 --- /dev/null +++ b/test/RestSharp.Tests.Serializers.Xml/SampleClasses/NestedElementTestClasses.cs @@ -0,0 +1,25 @@ +namespace RestSharp.Tests.Serializers.Xml.SampleClasses; + +// Test classes for nested element bugs + +public class Item { + public int Id { get; set; } + public List SubItems { get; set; } +} + +public class ItemContainer { + public List Items { get; set; } = new(); +} + +public class ItemWithGroup { + public int Id { get; set; } + public ItemGroup Group { get; set; } +} + +public class ItemGroup { + public List Items { get; set; } +} + +public class ItemsResponse { + public List Items { get; set; } = new(); +} diff --git a/test/RestSharp.Tests.Serializers.Xml/SampleClasses/Oddball.cs b/test/RestSharp.Tests.Serializers.Xml/SampleClasses/Oddball.cs new file mode 100644 index 000000000..53eedf2e9 --- /dev/null +++ b/test/RestSharp.Tests.Serializers.Xml/SampleClasses/Oddball.cs @@ -0,0 +1,16 @@ +using RestSharp.Serializers; + +namespace RestSharp.Tests.Serializers.Xml.SampleClasses; + +[DeserializeAs(Name = "oddballRootName")] +public class Oddball { + public string Sid { get; set; } + + public string FriendlyName { get; set; } + + [DeserializeAs(Name = "oddballPropertyName")] + public string GoodPropertyName { get; set; } + + [DeserializeAs(Name = "oddballListName")] + public List ListWithGoodName { get; set; } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Serializers.Xml/SampleClasses/SOUser.cs b/test/RestSharp.Tests.Serializers.Xml/SampleClasses/SOUser.cs new file mode 100644 index 000000000..807452dae --- /dev/null +++ b/test/RestSharp.Tests.Serializers.Xml/SampleClasses/SOUser.cs @@ -0,0 +1,17 @@ +namespace RestSharp.Tests.Serializers.Xml.SampleClasses; + +public class SoUser { + public int Id { get; set; } + public int Reputation { get; set; } + public long CreationDate { get; set; } + public string DisplayName { get; set; } + public string EmailHash { get; set; } + public string Age { get; set; } + public long LastAccessDate { get; set; } + public string WebsiteUrl { get; set; } + public string Location { get; set; } + public string AboutMe { get; set; } + public int Views { get; set; } + public int UpVotes { get; set; } + public int DownVotes { get; set; } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Serializers.Xml/SampleClasses/Struct.cs b/test/RestSharp.Tests.Serializers.Xml/SampleClasses/Struct.cs new file mode 100644 index 000000000..7a64ab5b7 --- /dev/null +++ b/test/RestSharp.Tests.Serializers.Xml/SampleClasses/Struct.cs @@ -0,0 +1,7 @@ +namespace RestSharp.Tests.Serializers.Xml.SampleClasses; + +public struct SimpleStruct { + public string One { get; set; } + public string Two { get; set; } + public int Three { get; set; } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Serializers.Xml/SampleClasses/TwilioCallList.cs b/test/RestSharp.Tests.Serializers.Xml/SampleClasses/TwilioCallList.cs new file mode 100644 index 000000000..127a30836 --- /dev/null +++ b/test/RestSharp.Tests.Serializers.Xml/SampleClasses/TwilioCallList.cs @@ -0,0 +1,11 @@ +namespace RestSharp.Tests.Serializers.Xml.SampleClasses; + +public class TwilioCallList : List { + public int Page { get; set; } + + public int NumPages { get; set; } +} + +public class Call { + public string Sid { get; set; } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Serializers.Xml/SampleClasses/eventful.cs b/test/RestSharp.Tests.Serializers.Xml/SampleClasses/eventful.cs new file mode 100644 index 000000000..57ab73c52 --- /dev/null +++ b/test/RestSharp.Tests.Serializers.Xml/SampleClasses/eventful.cs @@ -0,0 +1,109 @@ +namespace RestSharp.Tests.Serializers.Xml.SampleClasses; + +public class VenueSearch { + public string total_items { get; set; } + + public string page_size { get; set; } + + public string page_count { get; set; } + + public string page_number { get; set; } + + public string page_items { get; set; } + + public string first_item { get; set; } + + public string last_item { get; set; } + + public string search_time { get; set; } + + public List venues { get; set; } +} + +public class PerformerSearch { + public string total_items { get; set; } + + public string page_size { get; set; } + + public string page_count { get; set; } + + public string page_number { get; set; } + + public string page_items { get; set; } + + public string first_item { get; set; } + + public string last_item { get; set; } + + public string search_time { get; set; } + + public List performers { get; set; } +} + +public class Performer { + public string id { get; set; } + + public string url { get; set; } + + public string name { get; set; } + + public string short_bio { get; set; } + + public DateTime? created { get; set; } + + public string creator { get; set; } + + public string demand_count { get; set; } + + public string demand_member_count { get; set; } + + public string event_count { get; set; } + + public ServiceImage image { get; set; } +} + +public class Venue { + public string id { get; set; } + + public string url { get; set; } + + public string name { get; set; } + + public string venue_name { get; set; } + + public string description { get; set; } + + public string venue_type { get; set; } + + public string address { get; set; } + + public string city_name { get; set; } + + public string region_name { get; set; } + + public string postal_code { get; set; } + + public string country_name { get; set; } + + public string longitude { get; set; } + + public string latitude { get; set; } + + public string event_count { get; set; } +} + +public class ServiceImage { + public ServiceImage1 thumb { get; set; } + + public ServiceImage1 small { get; set; } + + public ServiceImage1 medium { get; set; } +} + +public class ServiceImage1 { + public string url { get; set; } + + public string width { get; set; } + + public string height { get; set; } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Serializers.Xml/SampleClasses/foursq.cs b/test/RestSharp.Tests.Serializers.Xml/SampleClasses/foursq.cs new file mode 100644 index 000000000..82e8d7439 --- /dev/null +++ b/test/RestSharp.Tests.Serializers.Xml/SampleClasses/foursq.cs @@ -0,0 +1,9 @@ +namespace RestSharp.Tests.Serializers.Xml.SampleClasses; + +public class VenuesResponse { + public List Groups { get; set; } +} + +public class Group { + public string Name { get; set; } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Serializers.Xml/SampleClasses/googleweather.cs b/test/RestSharp.Tests.Serializers.Xml/SampleClasses/googleweather.cs new file mode 100644 index 000000000..2e6d711cc --- /dev/null +++ b/test/RestSharp.Tests.Serializers.Xml/SampleClasses/googleweather.cs @@ -0,0 +1,65 @@ +namespace RestSharp.Tests.Serializers.Xml.SampleClasses; + +public class xml_api_reply { + public string version { get; set; } + + public Weather weather { get; set; } +} + +public class Weather : List { + public string module_id { get; set; } + + public string tab_id { get; set; } + + public string mobile_row { get; set; } + + public string mobile_zipped { get; set; } + + public string row { get; set; } + + public string section { get; set; } + + public Forecast_information forecast_information { get; set; } + + public Current_conditions current_conditions { get; set; } + + //public T forecast_conditions { get; set; } +} + +public class DataElement { + public string data { get; set; } +} + +public class Forecast_information { + public DataElement city { get; set; } + + public DataElement postal_code { get; set; } + + public DataElement forecast_date { get; set; } + + public DataElement unit_system { get; set; } +} + +public class Current_conditions { + public DataElement condition { get; set; } + + public DataElement temp_c { get; set; } + + public DataElement humidity { get; set; } + + public DataElement icon { get; set; } + + public DataElement wind_condition { get; set; } +} + +public class forecast_conditions { + public DataElement day_of_week { get; set; } + + public DataElement condition { get; set; } + + public DataElement low { get; set; } + + public DataElement high { get; set; } + + public DataElement icon { get; set; } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Serializers.Xml/SampleClasses/misc.cs b/test/RestSharp.Tests.Serializers.Xml/SampleClasses/misc.cs new file mode 100644 index 000000000..75777b12c --- /dev/null +++ b/test/RestSharp.Tests.Serializers.Xml/SampleClasses/misc.cs @@ -0,0 +1,271 @@ +using RestSharp.Serializers; + +// ReSharper disable UnusedAutoPropertyAccessor.Global +// ReSharper disable ClassNeverInstantiated.Global +// ReSharper disable CollectionNeverUpdated.Global + +namespace RestSharp.Tests.Serializers.Xml.SampleClasses; + +public class PersonForXml { + public string Name { get; set; } + + public DateTime StartDate { get; set; } + + public int Age { get; set; } + + public decimal Percent { get; set; } + + public long BigNumber { get; set; } + + public bool IsCool { get; set; } + + public List Friends { get; set; } + + public Friend BestFriend { get; set; } + + protected string Ignore { get; set; } + + public string IgnoreProxy => Ignore; + + protected string ReadOnly => null; + + public string ReadOnlyProxy => ReadOnly; + + public FoeList Foes { get; set; } + + public Guid UniqueId { get; set; } + + public Guid EmptyGuid { get; set; } + + public Uri Url { get; set; } + + public Uri UrlPath { get; set; } + + public Order Order { get; set; } + + public Disposition Disposition { get; set; } + + public Band FavoriteBand { get; set; } + + public class Band { + public string Name { get; set; } + } +} + +public class ValueCollectionForXml { + public string Value { get; set; } + public List Values { get; set; } +} + +public class ValueForXml { + public DateTime Timestamp { get; set; } + public string Value { get; set; } +} + +public class IncomingInvoice { + public int ConceptId { get; set; } +} + +public class PersonForJson { + public string Name { get; set; } + + public DateTime StartDate { get; set; } + + public int Age { get; set; } + + public decimal Percent { get; set; } + + public long BigNumber { get; set; } + + public bool IsCool { get; set; } + + public List Friends { get; set; } + + public Friend BestFriend { get; set; } + + public Guid Guid { get; set; } + + public Guid EmptyGuid { get; set; } + + public Uri Url { get; set; } + + public Uri UrlPath { get; set; } + + protected string Ignore { get; set; } + + public string IgnoreProxy => Ignore; + + protected string ReadOnly => null; + + public string ReadOnlyProxy => ReadOnly; + + public Dictionary Foes { get; set; } + + public Order Order { get; set; } + + public Disposition Disposition { get; set; } + + public int PersonId { get; set; } +} + +public enum Order { First, Second, Third } + +public enum Disposition { Friendly, SoSo, SteerVeryClear } + +public class Friend { + public string Name { get; set; } + + public int Since { get; set; } +} + +public class Foe { + public string Nickname { get; set; } +} + +public class FoeList : List { + public string Team { get; set; } +} + +public class Birthdate { + public DateTime Value { get; set; } +} + +public class OrderedProperties { + [SerializeAs(Index = 2)] + public string Name { get; set; } + + [SerializeAs(Index = 3)] + public int Age { get; set; } + + [SerializeAs(Index = 1)] + public DateTime StartDate { get; set; } +} + +public class ObjectProperties { + public object ObjectProperty { get; set; } +} + +public class DatabaseCollection : List; + +public class Database { + public string Name { get; set; } + + public string InitialCatalog { get; set; } + + public string DataSource { get; set; } +} + +public class Generic { + public T Data { get; set; } +} + +public class GenericWithList { + public List Items { get; set; } +} + +public class GuidList { + public List Ids { get; set; } +} + +public class DateTimeTestStructure { + public DateTime DateTime { get; set; } + + public DateTime? NullableDateTimeWithNull { get; set; } + + public DateTime? NullableDateTimeWithValue { get; set; } + + public DateTimeOffset DateTimeOffset { get; set; } + + public DateTimeOffset? NullableDateTimeOffsetWithNull { get; set; } + + public DateTimeOffset? NullableDateTimeOffsetWithValue { get; set; } +} + +public class Iso8601DateTimeTestStructure { + public DateTime DateTimeLocal { get; set; } + + public DateTime DateTimeUtc { get; set; } + + public DateTime DateTimeWithOffset { get; set; } +} + +public class NewDateTimeTestStructure { + public DateTime DateTime { get; set; } + + public DateTime DateTimeNegative { get; set; } +} + +public class TimeSpanTestStructure { + public TimeSpan Tick { get; set; } + + public TimeSpan Millisecond { get; set; } + + public TimeSpan Second { get; set; } + + public TimeSpan Minute { get; set; } + + public TimeSpan Hour { get; set; } + + public TimeSpan? NullableWithoutValue { get; set; } + + public TimeSpan? NullableWithValue { get; set; } + + public TimeSpan? IsoSecond { get; set; } + + public TimeSpan? IsoMinute { get; set; } + + public TimeSpan? IsoHour { get; set; } + + public TimeSpan? IsoDay { get; set; } + + public TimeSpan? IsoMonth { get; set; } + + public TimeSpan? IsoYear { get; set; } +} + +public class JsonEnumsTestStructure { + public Disposition Upper { get; set; } + + public Disposition Lower { get; set; } + + public Disposition CamelCased { get; set; } + + public Disposition Underscores { get; set; } + + public Disposition LowerUnderscores { get; set; } + + public Disposition Dashes { get; set; } + + public Disposition LowerDashes { get; set; } + + public Disposition Integer { get; set; } +} + +public class DecimalNumber { + public decimal Value { get; set; } +} + +public class Note { + public const string ConstTitle = "What a note."; + public const string ConstMessage = "Content"; + + [SerializeAs(Attribute = true), DeserializeAs(Attribute = true)] + public int Id { get; set; } + + [SerializeAs(Content = true), DeserializeAs(Content = true)] + public string Message { get; set; } + + public string Title { get; set; } +} + +public class WrongNote { + [SerializeAs(Content = true), DeserializeAs(Content = true)] + public int Id { get; set; } + + [SerializeAs(Content = true), DeserializeAs(Content = true)] + public string Text { get; set; } +} + +public class DateTimeResponse { + public DateTime CreatedOn { get; set; } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Serializers.Xml/SampleClasses/nullables.cs b/test/RestSharp.Tests.Serializers.Xml/SampleClasses/nullables.cs new file mode 100644 index 000000000..5cfe46a80 --- /dev/null +++ b/test/RestSharp.Tests.Serializers.Xml/SampleClasses/nullables.cs @@ -0,0 +1,9 @@ +namespace RestSharp.Tests.Serializers.Xml.SampleClasses; + +public class NullableValues { + public int? Id { get; set; } + + public DateTime? StartDate { get; set; } + + public Guid? UniqueId { get; set; } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Serializers.Xml/SampleClasses/twitter.cs b/test/RestSharp.Tests.Serializers.Xml/SampleClasses/twitter.cs new file mode 100644 index 000000000..1afe25fc3 --- /dev/null +++ b/test/RestSharp.Tests.Serializers.Xml/SampleClasses/twitter.cs @@ -0,0 +1,118 @@ +using RestSharp.Serializers; + +// ReSharper disable InconsistentNaming +// ReSharper disable UnusedMember.Global +// ReSharper disable ClassNeverInstantiated.Global +#pragma warning disable CS8981 // The type name only contains lower-cased ascii characters. Such names may become reserved for the language. +#pragma warning disable CS8981 + +namespace RestSharp.Tests.Serializers.Xml.SampleClasses; + +#pragma warning disable CS8981 +public class status { + public bool truncated { get; set; } + + public string created_at { get; set; } + + public string source { get; set; } + + public bool favorited { get; set; } + + public string in_reply_to_user_id { get; set; } + + public string in_reply_to_status_id { get; set; } + + public string in_reply_to_screen_name { get; set; } + + // ignore contributors for now + public user user { get; set; } + + // ignore geo + public long id { get; set; } + + public string text { get; set; } +} + +public class user { + public string url { get; set; } + + public string description { get; set; } + + public string profile_text_color { get; set; } + + public int followers_count { get; set; } + + public int statuses_count { get; set; } + + public bool geo_enabled { get; set; } + + public string profile_background_image_url { get; set; } + + public bool notifications { get; set; } + + public string created_at { get; set; } + + public int friends_count { get; set; } + + public string profile_link_color { get; set; } + + public bool contributors_enabled { get; set; } + + public bool profile_background_tile { get; set; } + + public int favourites_count { get; set; } + + public string profile_background_color { get; set; } + + public string profile_image_url { get; set; } + + public string lang { get; set; } + + public bool verified { get; set; } + + public string profile_sidebar_fill_color { get; set; } + + public bool @protected { get; set; } + + public string screen_name { get; set; } + + public bool following { get; set; } + + public string location { get; set; } + + public string name { get; set; } + + public string time_zone { get; set; } + + public string profile_sidebar_border_color { get; set; } + + public long id { get; set; } + + public int utc_offset { get; set; } +} + +public class complexStatus { + public bool truncated { get; set; } + + public string created_at { get; set; } + + public string source { get; set; } + + public bool favorited { get; set; } + + public string in_reply_to_user_id { get; set; } + + public string in_reply_to_status_id { get; set; } + + public string in_reply_to_screen_name { get; set; } + + // ignore contributors for now + [DeserializeAs(Name = "user.following")] + public bool follow { get; set; } + + // ignore geo + public long id { get; set; } + + public string text { get; set; } +} +#pragma warning restore CS8981 \ No newline at end of file diff --git a/test/RestSharp.Tests.Serializers.Xml/SampleData/Goodreads.xml b/test/RestSharp.Tests.Serializers.Xml/SampleData/Goodreads.xml new file mode 100644 index 000000000..f5e64aa91 --- /dev/null +++ b/test/RestSharp.Tests.Serializers.Xml/SampleData/Goodreads.xml @@ -0,0 +1,17 @@ + + + + + + 0345475836 + + + + + 1198344567 + + 0802775802 + + + + \ No newline at end of file diff --git a/test/RestSharp.Tests.Serializers.Xml/SampleData/GoodreadsFormatError.xml b/test/RestSharp.Tests.Serializers.Xml/SampleData/GoodreadsFormatError.xml new file mode 100644 index 000000000..c65f67bce --- /dev/null +++ b/test/RestSharp.Tests.Serializers.Xml/SampleData/GoodreadsFormatError.xml @@ -0,0 +1,17 @@ + + + + + + 0345475836 + + + + + 1198344567 + + 0802775802 + + + + \ No newline at end of file diff --git a/RestSharp.Tests/SampleData/GoogleWeather.xml b/test/RestSharp.Tests.Serializers.Xml/SampleData/GoogleWeather.xml similarity index 100% rename from RestSharp.Tests/SampleData/GoogleWeather.xml rename to test/RestSharp.Tests.Serializers.Xml/SampleData/GoogleWeather.xml diff --git a/test/RestSharp.Tests.Serializers.Xml/SampleData/InlineListSample.xml b/test/RestSharp.Tests.Serializers.Xml/SampleData/InlineListSample.xml new file mode 100644 index 000000000..a8eaf8f2f --- /dev/null +++ b/test/RestSharp.Tests.Serializers.Xml/SampleData/InlineListSample.xml @@ -0,0 +1,8 @@ + + + 4 + value1 + value2 + value3 + value4 + diff --git a/test/RestSharp.Tests.Serializers.Xml/SampleData/Lastfm.xml b/test/RestSharp.Tests.Serializers.Xml/SampleData/Lastfm.xml new file mode 100644 index 000000000..8611fbd97 --- /dev/null +++ b/test/RestSharp.Tests.Serializers.Xml/SampleData/Lastfm.xml @@ -0,0 +1,46 @@ + + + 328799 + Philip Glass + + Philip Glass + Orchestra and Chorus of Erfurt Theatre + Philip Glass + + + 8777860 + Barbican Centre + + London + United Kingdom + Silk Street + EC2Y 8DS + + 51.520099 + -0.093609 + + + https://www.last.fm/venue/8777860+Barbican+Centre + https://www.barbican.org.uk + + https://userserve-ak.last.fm/serve/34/418510.jpg + https://userserve-ak.last.fm/serve/64/418510.jpg + https://userserve-ak.last.fm/serve/126/418510.jpg + https://userserve-ak.last.fm/serve/252/418510.jpg + https://userserve-ak.last.fm/serve/500/418510/Barbican+Centre.jpg + + Thu, 12 Jun 2008 19:30:00 + + https://userserve-ak.last.fm/serve/34/39466081.png + https://userserve-ak.last.fm/serve/64/39466081.png + https://userserve-ak.last.fm/serve/126/39466081.png + https://userserve-ak.last.fm/serve/252/39466081.png + 48 + 0 + lastfm:event=328799 + https://www.last.fm/event/328799+Philip+Glass+at+Barbican+Centre+on+12+June+2008 + + + 0 + + diff --git a/test/RestSharp.Tests.Serializers.Xml/SampleData/ListWithAttributes.xml b/test/RestSharp.Tests.Serializers.Xml/SampleData/ListWithAttributes.xml new file mode 100644 index 000000000..9c4dab64f --- /dev/null +++ b/test/RestSharp.Tests.Serializers.Xml/SampleData/ListWithAttributes.xml @@ -0,0 +1,55 @@ + + + + + CA92d4405c9237c4ea04b56cbda88e128c + Fri, 13 Aug 2010 01:16:22 +0000 + Fri, 13 Aug 2010 01:16:22 +0000 + + AC5ef877a5fe4238be081ea6f3c44186f3 + +15304551166 + +15105555555 + PNe2d8e63b37f46f2adb16f228afdb9058 + queued + Thu, 12 Aug 2010 01:37:05 +0000 + Thu, 12 Aug 2010 01:37:40 +0000 + + + outbound-api + + 2010-04-01 + + + /2010-04-01/Accounts/AC5ef877a5fe4238be081ea6f3c44186f3/Calls/CA92d4405c9237c4ea04b56cbda88e128c + + /2010-04-01/Accounts/AC5ef877a5fe4238be081ea6f3c44186f3/Calls/CA92d4405c9237c4ea04b56cbda88e128c/Notifications + /2010-04-01/Accounts/AC5ef877a5fe4238be081ea6f3c44186f3/Calls/CA92d4405c9237c4ea04b56cbda88e128c/Recordings + + + + CA12345 + Fri, 13 Aug 2010 01:16:22 +0000 + Fri, 13 Aug 2010 01:16:22 +0000 + + AC5ef877a5fe4238be081ea6f3c44186f3 + +15304551166 + +15105555555 + PNe2d8e63b37f46f2adb16f228afdb9058 + queued + Thu, 12 Aug 2010 01:37:05 +0000 + Thu, 12 Aug 2010 01:37:40 +0000 + + + outbound-api + + 2010-04-01 + + + /2010-04-01/Accounts/AC5ef877a5fe4238be081ea6f3c44186f3/Calls/CA92d4405c9237c4ea04b56cbda88e128c + + /2010-04-01/Accounts/AC5ef877a5fe4238be081ea6f3c44186f3/Calls/CA92d4405c9237c4ea04b56cbda88e128c/Notifications + /2010-04-01/Accounts/AC5ef877a5fe4238be081ea6f3c44186f3/Calls/CA92d4405c9237c4ea04b56cbda88e128c/Recordings + + + + \ No newline at end of file diff --git a/test/RestSharp.Tests.Serializers.Xml/SampleData/NestedListSample.xml b/test/RestSharp.Tests.Serializers.Xml/SampleData/NestedListSample.xml new file mode 100644 index 000000000..9e52e52d6 --- /dev/null +++ b/test/RestSharp.Tests.Serializers.Xml/SampleData/NestedListSample.xml @@ -0,0 +1,9 @@ + + + + value1 + value2 + value3 + value4 + + diff --git a/RestSharp.Tests/SampleData/boolean_from_number.xml b/test/RestSharp.Tests.Serializers.Xml/SampleData/boolean_from_number.xml similarity index 100% rename from RestSharp.Tests/SampleData/boolean_from_number.xml rename to test/RestSharp.Tests.Serializers.Xml/SampleData/boolean_from_number.xml diff --git a/RestSharp.Tests/SampleData/boolean_from_string.xml b/test/RestSharp.Tests.Serializers.Xml/SampleData/boolean_from_string.xml similarity index 100% rename from RestSharp.Tests/SampleData/boolean_from_string.xml rename to test/RestSharp.Tests.Serializers.Xml/SampleData/boolean_from_string.xml diff --git a/test/RestSharp.Tests.Serializers.Xml/SampleData/deserialize_as_list.xml b/test/RestSharp.Tests.Serializers.Xml/SampleData/deserialize_as_list.xml new file mode 100644 index 000000000..3509f55fb --- /dev/null +++ b/test/RestSharp.Tests.Serializers.Xml/SampleData/deserialize_as_list.xml @@ -0,0 +1,17 @@ + + + + + 1 + Jackson + oddball + + + + + 1 + Jackson + evenball + + + \ No newline at end of file diff --git a/test/RestSharp.Tests.Serializers.Xml/SampleData/directlists.xml b/test/RestSharp.Tests.Serializers.Xml/SampleData/directlists.xml new file mode 100644 index 000000000..4c554b287 --- /dev/null +++ b/test/RestSharp.Tests.Serializers.Xml/SampleData/directlists.xml @@ -0,0 +1,21 @@ + + + + sv + lusrvsql3 + Pomtest + Pomtest + + + + + + sv + lusrvsql3 + testdb + Test + + + + + diff --git a/test/RestSharp.Tests.Serializers.Xml/SampleData/eventful.xml b/test/RestSharp.Tests.Serializers.Xml/SampleData/eventful.xml new file mode 100644 index 000000000..330922ca9 --- /dev/null +++ b/test/RestSharp.Tests.Serializers.Xml/SampleData/eventful.xml @@ -0,0 +1,111 @@ + + + 3 + 10 + 1 + 1 + 3 + 1 + 3 + 0.262 + + + https://eventful.com/venues/tivoli-/V0-001-002011236-6 + Tivoli + Tivoli + + +
291 Dandenong Rd
+ Prahran + Victoria + VIC + 3181 + Australia + AU + AUS + 145.004726 + -37.859123 + EVDB Geocoder + evdb + + + 0 + 0 + 0 + 0 + +
+ + https://eventful.com/brisbane/venues/tivoli-/V0-001-002169294-8 + Tivoli + Tivoli + <br> <br>(No phone for this venue yet.)<br> <br>(No website for this venue yet.)<br> <br> + +
Brisbane
+ Brisbane + Queensland + QLD + + Australia + AU + AUS + 153.017 + -27.5 + City Based GeoCodes + evdb + + + 1 + 0 + 0 + 1 + +
+ + https://eventful.com/brisbane/venues/the-tivoli-/V0-001-000266914-3 + The Tivoli + The Tivoli + Built in 1917 and restored in Art Deco style, this elegant, fully licensed theatre has state-of-the-art technical features, top-of-the-range facilities, in-house catering by our 5 star chef and dedicated staff available to meet all your needs. + Concert Hall +
52 Costin Street Fortitude Valley
+ Brisbane + Queensland + QLD + 4006 + Australia + AU + AUS + 153.031548 + -27.452335 + EVDB Geocoder + evdb + + + 28 + 0 + 7 + 1 + + https://images.evdb.com/images/small/I0-001/001/414/278-2.jpeg + 48 + 48 + + + https://images.evdb.com/images/thumb/I0-001/001/414/278-2.jpeg + 48 + 48 + + + https://images.evdb.com/images/small/I0-001/001/414/278-2.jpeg + 48 + 48 + + + https://images.evdb.com/images/medium/I0-001/001/414/278-2.jpeg + 128 + 128 + + +
+
+
\ No newline at end of file diff --git a/test/RestSharp.Tests.Serializers.Xml/SampleData/header_and_rows.xml b/test/RestSharp.Tests.Serializers.Xml/SampleData/header_and_rows.xml new file mode 100644 index 000000000..61c983106 --- /dev/null +++ b/test/RestSharp.Tests.Serializers.Xml/SampleData/header_and_rows.xml @@ -0,0 +1,20 @@ + + + +
+ text title + text body + 2014-12-12 +
+ + + first row text 1 sample + first row text 2 sample + + + second row text 1 sample + second row text 2 sample + + +
+
\ No newline at end of file diff --git a/restsharp.nuspec b/test/RestSharp.Tests.Serializers.Xml/SampleData/restsharp.nuspec similarity index 100% rename from restsharp.nuspec rename to test/RestSharp.Tests.Serializers.Xml/SampleData/restsharp.nuspec diff --git a/test/RestSharp.Tests.Serializers.Xml/SampleData/xmllists.xml b/test/RestSharp.Tests.Serializers.Xml/SampleData/xmllists.xml new file mode 100644 index 000000000..1c3d1b4c1 --- /dev/null +++ b/test/RestSharp.Tests.Serializers.Xml/SampleData/xmllists.xml @@ -0,0 +1,18 @@ + + + + John + Bob + Albert + Jeff + Charlie + + + 1 + 2 + 3 + 5 + 7 + 11 + + diff --git a/test/RestSharp.Tests.Serializers.Xml/XmlAttributeDeserializerTests.cs b/test/RestSharp.Tests.Serializers.Xml/XmlAttributeDeserializerTests.cs new file mode 100644 index 000000000..74dcf041a --- /dev/null +++ b/test/RestSharp.Tests.Serializers.Xml/XmlAttributeDeserializerTests.cs @@ -0,0 +1,876 @@ +using System.Globalization; +using System.Xml.Linq; +using RestSharp.Serializers.Xml; +using RestSharp.Tests.Serializers.Xml.SampleClasses; + +namespace RestSharp.Tests.Serializers.Xml; + +public class XmlAttributeDeserializerTests { + const string GuidString = "AC1FC4BC-087A-4242-B8EE-C53EBE9887A5"; + +#if NET + readonly string _sampleDataPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "SampleData"); +#else + readonly string _sampleDataPath = Path.Combine(Directory.GetCurrentDirectory(), "SampleData"); +#endif + + string PathFor(string sampleFile) => Path.Combine(_sampleDataPath, sampleFile); + + [Fact] + public void Can_Deserialize_Lists_of_Simple_Types() { + var xmlPath = PathFor("xmllists.xml"); + var doc = XDocument.Load(xmlPath); + var xml = new XmlAttributeDeserializer(); + + var output = xml.Deserialize(new() { Content = doc.ToString() })!; + + Assert.NotEmpty(output.Numbers); + Assert.NotEqual(0, output.Names[0].Length); + Assert.NotEqual(0, output.Numbers.Sum()); + } + + [Fact] + public void Can_Deserialize_To_List_Inheritor_From_Custom_Root_With_Attributes() { + var xmlPath = PathFor("ListWithAttributes.xml"); + var doc = XDocument.Load(xmlPath); + var xml = new XmlAttributeDeserializer { RootElement = "Calls" }; + var output = xml.Deserialize(new() { Content = doc.ToString() })!; + + Assert.Equal(3, output.NumPages); + Assert.Equal(2, output.Count); + } + + [Fact] + public void Can_Deserialize_To_Standalone_List_Without_Matching_Class_Case() { + var xmlPath = PathFor("InlineListSample.xml"); + var doc = XDocument.Load(xmlPath); + var xml = new XmlAttributeDeserializer(); + var output = xml.Deserialize>(new() { Content = doc.ToString() })!; + + Assert.Equal(4, output.Count); + } + + [Fact] + public void Can_Deserialize_To_Standalone_List_With_Matching_Class_Case() { + var xmlPath = PathFor("InlineListSample.xml"); + var doc = XDocument.Load(xmlPath); + var xml = new XmlAttributeDeserializer(); + var output = xml.Deserialize>(new() { Content = doc.ToString() })!; + + Assert.Equal(4, output.Count); + } + + [Fact] + public void Can_Deserialize_Directly_To_Lists_Off_Root_Element() { + var xmlPath = PathFor("directlists.xml"); + var doc = XDocument.Load(xmlPath); + var xml = new XmlAttributeDeserializer(); + var output = xml.Deserialize>(new() { Content = doc.ToString() })!; + + Assert.Equal(2, output.Count); + } + + [Fact] + public void Can_Deserialize_Parentless_aka_Inline_List_Items_Without_Matching_Class_Name() { + var xmlPath = PathFor("InlineListSample.xml"); + var doc = XDocument.Load(xmlPath); + var xml = new XmlAttributeDeserializer(); + var output = xml.Deserialize(new() { Content = doc.ToString() })!; + + Assert.Equal(4, output.Images.Count); + } + + [Fact] + public void Can_Deserialize_Parentless_aka_Inline_List_Items_With_Matching_Class_Name() { + var xmlpath = PathFor("InlineListSample.xml"); + var doc = XDocument.Load(xmlpath); + var xml = new XmlAttributeDeserializer(); + var output = xml.Deserialize(new() { Content = doc.ToString() })!; + + Assert.Equal(4, output.images.Count); + } + + [Fact] + public void Can_Deserialize_Parentless_aka_Inline_List_Items_With_Matching_Class_Name_With_Additional_Property() { + var xmlpath = PathFor("InlineListSample.xml"); + var doc = XDocument.Load(xmlpath); + var xml = new XmlAttributeDeserializer(); + var output = xml.Deserialize(new() { Content = doc.ToString() })!; + + Assert.Equal(4, output.Count); + } + + [Fact] + public void Can_Deserialize_Nested_List_Items_Without_Matching_Class_Name() { + var xmlpath = PathFor("NestedListSample.xml"); + var doc = XDocument.Load(xmlpath); + var xml = new XmlAttributeDeserializer(); + var output = xml.Deserialize(new() { Content = doc.ToString() })!; + + Assert.Equal(4, output.Images.Count); + } + + [Fact] + public void Can_Deserialize_Nested_List_Items_With_Matching_Class_Name() { + var xmlpath = PathFor("NestedListSample.xml"); + var doc = XDocument.Load(xmlpath); + var xml = new XmlAttributeDeserializer(); + var output = xml.Deserialize(new() { Content = doc.ToString() })!; + + Assert.Equal(4, output.images.Count); + } + + [Fact] + public void Can_Deserialize_Nested_List_Without_Elements_To_Empty_List() { + var doc = CreateXmlWithEmptyNestedList(); + var xml = new XmlAttributeDeserializer(); + var output = xml.Deserialize(new() { Content = doc })!; + + Assert.NotNull(output.Images); + Assert.Empty(output.Images); + } + + [Fact] + public void Can_Deserialize_Inline_List_Without_Elements_To_Empty_List() { + var doc = CreateXmlWithEmptyInlineList(); + var xml = new XmlAttributeDeserializer(); + var output = xml.Deserialize(new() { Content = doc })!; + + Assert.NotNull(output.Images); + Assert.Empty(output.Images); + } + + [Fact] + public void Can_Deserialize_Empty_Elements_to_Nullable_Values() { + var doc = CreateXmlWithNullValues(); + var xml = new XmlAttributeDeserializer(); + var output = xml.Deserialize(new() { Content = doc })!; + + Assert.Null(output.Id); + Assert.Null(output.StartDate); + Assert.Null(output.UniqueId); + } + + [Fact] + public void Can_Deserialize_Elements_to_Nullable_Values() { + var culture = CultureInfo.InvariantCulture; + var doc = CreateXmlWithoutEmptyValues(culture); + + var xml = new XmlAttributeDeserializer { Culture = culture }; + var output = xml.Deserialize(new() { Content = doc })!; + + Assert.NotNull(output.Id); + Assert.NotNull(output.StartDate); + Assert.NotNull(output.UniqueId); + Assert.Equal(123, output.Id); + Assert.Equal(new DateTime(2010, 2, 21, 9, 35, 00), output.StartDate); + Assert.Equal(new Guid(GuidString), output.UniqueId); + } + + [Fact] + public void Can_Deserialize_TimeSpan() { + var culture = CultureInfo.InvariantCulture; + var doc = new XDocument(culture); + + TimeSpan? nullTimespan = null; + TimeSpan? nullValueTimeSpan = new TimeSpan(21, 30, 7); + + var root = new XElement("Person"); + + root.Add(new XElement("Tick", new TimeSpan(468006))); + root.Add(new XElement("Millisecond", new TimeSpan(0, 0, 0, 0, 125))); + root.Add(new XElement("Second", new TimeSpan(0, 0, 8))); + root.Add(new XElement("Minute", new TimeSpan(0, 55, 2))); + root.Add(new XElement("Hour", new TimeSpan(21, 30, 7))); + root.Add(new XElement("NullableWithoutValue", nullTimespan)); + root.Add(new XElement("NullableWithValue", nullValueTimeSpan)); + + doc.Add(root); + + var response = new RestResponse { Content = doc.ToString() }; + var d = new XmlAttributeDeserializer { Culture = culture }; + var payload = d.Deserialize(response)!; + + Assert.Equal(new(468006), payload.Tick); + Assert.Equal(new(0, 0, 0, 0, 125), payload.Millisecond); + Assert.Equal(new(0, 0, 8), payload.Second); + Assert.Equal(new(0, 55, 2), payload.Minute); + Assert.Equal(new(21, 30, 7), payload.Hour); + Assert.Null(payload.NullableWithoutValue); + Assert.NotNull(payload.NullableWithValue); + Assert.Equal(new(21, 30, 7), payload.NullableWithValue.Value); + } + + [Fact] + public void Can_Deserialize_Custom_Formatted_Date() { + const string format = "dd yyyy MMM, hh:mm ss tt zzz"; + + var culture = CultureInfo.InvariantCulture; + var date = new DateTime(2010, 2, 8, 11, 11, 11); + var doc = new XDocument(); + var root = new XElement("Person"); + + root.Add(new XElement("StartDate", date.ToString(format, culture))); + + doc.Add(root); + + var xml = new XmlAttributeDeserializer { + DateFormat = format, + Culture = culture + }; + var response = new RestResponse { Content = doc.ToString() }; + var output = xml.Deserialize(response)!; + + Assert.Equal(date, output.StartDate); + } + + [Fact] + public void Can_Deserialize_Nested_Class() { + var doc = CreateElementsXml(); + var response = new RestResponse { Content = doc }; + var d = new XmlAttributeDeserializer(); + var p = d.Deserialize(response); + + Assert.NotNull(p.FavoriteBand); + Assert.Equal("Goldfinger", p.FavoriteBand.Name); + } + + [Fact] + public void Can_Deserialize_Elements_On_Default_Root() { + var doc = CreateElementsXml(); + var response = new RestResponse { Content = doc }; + var d = new XmlAttributeDeserializer(); + var p = d.Deserialize(response); + + Assert.Equal("John Sheehan", p.Name); + Assert.Equal(new(2009, 9, 25, 0, 6, 1), p.StartDate); + Assert.Equal(28, p.Age); + Assert.Equal(long.MaxValue, p.BigNumber); + Assert.Equal(99.9999m, p.Percent); + Assert.False(p.IsCool); + Assert.Equal(new(GuidString), p.UniqueId); + Assert.Equal(Guid.Empty, p.EmptyGuid); + Assert.Equal(new("http://example.com", UriKind.RelativeOrAbsolute), p.Url); + Assert.Equal(new("/foo/bar", UriKind.RelativeOrAbsolute), p.UrlPath); + Assert.Equal(Order.Third, p.Order); + Assert.Equal(Disposition.SoSo, p.Disposition); + Assert.NotNull(p.Friends); + Assert.Equal(10, p.Friends.Count); + Assert.NotNull(p.BestFriend); + Assert.Equal("The Fonz", p.BestFriend.Name); + Assert.Equal(1952, p.BestFriend.Since); + } + + [Fact] + public void Can_Deserialize_Attributes_On_Default_Root() { + var doc = CreateAttributesXml(); + var response = new RestResponse { Content = doc }; + var d = new XmlAttributeDeserializer(); + var p = d.Deserialize(response); + + Assert.Equal("John Sheehan", p.Name); + Assert.Equal(new (2009, 9, 25, 0, 6, 1), p.StartDate); + Assert.Equal(28, p.Age); + Assert.Equal(long.MaxValue, p.BigNumber); + Assert.Equal(99.9999m, p.Percent); + Assert.False(p.IsCool); + Assert.Equal(new (GuidString), p.UniqueId); + Assert.Equal(new ("http://example.com", UriKind.RelativeOrAbsolute), p.Url); + Assert.Equal(new ("/foo/bar", UriKind.RelativeOrAbsolute), p.UrlPath); + Assert.NotNull(p.BestFriend); + Assert.Equal("The Fonz", p.BestFriend.Name); + Assert.Equal(1952, p.BestFriend.Since); + } + + [Fact] + public void Ignore_Protected_Property_That_Exists_In_Data() { + var doc = CreateElementsXml(); + var response = new RestResponse { Content = doc }; + var d = new XmlAttributeDeserializer(); + var p = d.Deserialize(response); + + Assert.Null(p.IgnoreProxy); + } + + [Fact] + public void Ignore_ReadOnly_Property_That_Exists_In_Data() { + var doc = CreateElementsXml(); + var response = new RestResponse { Content = doc }; + var d = new XmlAttributeDeserializer(); + var p = d.Deserialize(response); + + Assert.Null(p.ReadOnlyProxy); + } + + [Fact] + public void Can_Deserialize_Names_With_Underscores_On_Default_Root() { + var doc = CreateUnderscoresXml(); + var response = new RestResponse { Content = doc }; + var d = new XmlAttributeDeserializer(); + var p = d.Deserialize(response); + + Assert.Equal("John Sheehan", p.Name); + Assert.Equal(new (2009, 9, 25, 0, 6, 1), p.StartDate); + Assert.Equal(28, p.Age); + Assert.Equal(long.MaxValue, p.BigNumber); + Assert.Equal(99.9999m, p.Percent); + Assert.False(p.IsCool); + Assert.Equal(new (GuidString), p.UniqueId); + Assert.Equal(new ("http://example.com", UriKind.RelativeOrAbsolute), p.Url); + Assert.Equal(new ("/foo/bar", UriKind.RelativeOrAbsolute), p.UrlPath); + Assert.NotNull(p.Friends); + Assert.Equal(10, p.Friends.Count); + Assert.NotNull(p.BestFriend); + Assert.Equal("The Fonz", p.BestFriend.Name); + Assert.Equal(1952, p.BestFriend.Since); + Assert.NotNull(p.Foes); + Assert.Equal(5, p.Foes.Count); + Assert.Equal("Yankees", p.Foes.Team); + } + + [Fact] + public void Can_Deserialize_Names_With_Dashes_On_Default_Root() { + var doc = CreateDashesXml(); + var response = new RestResponse { Content = doc }; + var d = new XmlAttributeDeserializer(); + var p = d.Deserialize(response); + + Assert.Equal("John Sheehan", p.Name); + Assert.Equal(new (2009, 9, 25, 0, 6, 1), p.StartDate); + Assert.Equal(28, p.Age); + Assert.Equal(long.MaxValue, p.BigNumber); + Assert.Equal(99.9999m, p.Percent); + Assert.False(p.IsCool); + Assert.Equal(new (GuidString), p.UniqueId); + Assert.Equal(new ("http://example.com", UriKind.RelativeOrAbsolute), p.Url); + Assert.Equal(new ("/foo/bar", UriKind.RelativeOrAbsolute), p.UrlPath); + Assert.NotNull(p.Friends); + Assert.Equal(10, p.Friends.Count); + Assert.NotNull(p.BestFriend); + Assert.Equal("The Fonz", p.BestFriend.Name); + Assert.Equal(1952, p.BestFriend.Since); + Assert.NotNull(p.Foes); + Assert.Equal(5, p.Foes.Count); + Assert.Equal("Yankees", p.Foes.Team); + } + + [Fact] + public void Can_Deserialize_Names_With_Underscores_Without_Matching_Case_On_Default_Root() { + var doc = CreateLowercaseUnderscoresXml(); + var response = new RestResponse { Content = doc }; + var d = new XmlAttributeDeserializer(); + var p = d.Deserialize(response); + + Assert.Equal("John Sheehan", p.Name); + Assert.Equal(new (2009, 9, 25, 0, 6, 1), p.StartDate); + Assert.Equal(28, p.Age); + Assert.Equal(long.MaxValue, p.BigNumber); + Assert.Equal(99.9999m, p.Percent); + Assert.False(p.IsCool); + Assert.Equal(new (GuidString), p.UniqueId); + Assert.Equal(new ("http://example.com", UriKind.RelativeOrAbsolute), p.Url); + Assert.Equal(new ("/foo/bar", UriKind.RelativeOrAbsolute), p.UrlPath); + Assert.NotNull(p.Friends); + Assert.Equal(10, p.Friends.Count); + Assert.NotNull(p.BestFriend); + Assert.Equal("The Fonz", p.BestFriend.Name); + Assert.Equal(1952, p.BestFriend.Since); + Assert.NotNull(p.Foes); + Assert.Equal(5, p.Foes.Count); + Assert.Equal("Yankees", p.Foes.Team); + } + + [Fact] + public void Can_Deserialize_Lower_Cased_Root_Elements_With_Dashes() { + var doc = CreateDashesXml(); + var response = new RestResponse { Content = doc }; + var d = new XmlAttributeDeserializer(); + var p = d.Deserialize(response); + + Assert.Equal("John Sheehan", p.Name); + Assert.Equal(new (2009, 9, 25, 0, 6, 1), p.StartDate); + Assert.Equal(28, p.Age); + Assert.Equal(long.MaxValue, p.BigNumber); + Assert.Equal(99.9999m, p.Percent); + Assert.False(p.IsCool); + Assert.Equal(new (GuidString), p.UniqueId); + Assert.Equal(new ("http://example.com", UriKind.RelativeOrAbsolute), p.Url); + Assert.Equal(new ("/foo/bar", UriKind.RelativeOrAbsolute), p.UrlPath); + Assert.NotNull(p.Friends); + Assert.Equal(10, p.Friends.Count); + Assert.NotNull(p.BestFriend); + Assert.Equal("The Fonz", p.BestFriend.Name); + Assert.Equal(1952, p.BestFriend.Since); + Assert.NotNull(p.Foes); + Assert.Equal(5, p.Foes.Count); + Assert.Equal("Yankees", p.Foes.Team); + } + + [Fact] + public void Can_Deserialize_Root_Elements_Without_Matching_Case_And_Dashes() { + var doc = CreateLowerCasedRootElementWithDashesXml(); + var response = new RestResponse { Content = doc }; + var d = new XmlAttributeDeserializer(); + var p = d.Deserialize>(response); + + Assert.NotNull(p); + Assert.Single(p); + Assert.Equal(45, p[0].ConceptId); + } + + [Fact] + public void Can_Deserialize_Eventful_Xml() { + var xmlFilePath = PathFor("eventful.xml"); + var doc = XDocument.Load(xmlFilePath); + var response = new RestResponse { Content = doc.ToString() }; + var d = new XmlAttributeDeserializer(); + var output = d.Deserialize(response)!; + + Assert.Equal(3, output.venues.Count); + Assert.Equal("Tivoli", output.venues[0].name); + Assert.Equal("https://eventful.com/brisbane/venues/tivoli-/V0-001-002169294-8", output.venues[1].url); + Assert.Equal("V0-001-000266914-3", output.venues[2].id); + } + + [Fact] + public void Can_Deserialize_Lastfm_Xml() { + var xmlFilePath = PathFor("Lastfm.xml"); + var doc = XDocument.Load(xmlFilePath); + var response = new RestResponse { Content = doc.ToString() }; + var d = new XmlAttributeDeserializer(); + var output = d.Deserialize(response)!; + + Assert.Equal("https://www.last.fm/event/328799+Philip+Glass+at+Barbican+Centre+on+12+June+2008", output.url); + Assert.Equal("https://www.last.fm/venue/8777860+Barbican+Centre", output.venue.url); + } + + [Fact] + public void Can_Deserialize_Google_Weather_Xml() { + var xmlFilePath = PathFor("GoogleWeather.xml"); + var doc = XDocument.Load(xmlFilePath); + var response = new RestResponse { Content = doc.ToString() }; + var d = new XmlAttributeDeserializer(); + var output = d.Deserialize(response)!; + + Assert.Equal(4, output.weather.Count); + Assert.Equal("Sunny", output.weather[0].condition.data); + } + + [Fact] + public void Can_Deserialize_Google_Weather_Xml_WithDeserializeAs() { + var xmlFilePath = PathFor("GoogleWeather.xml"); + var doc = XDocument.Load(xmlFilePath); + var response = new RestResponse { Content = doc.ToString() }; + var d = new XmlAttributeDeserializer(); + var output = d.Deserialize(response)!; + + Assert.Equal(4, output.Weather.Count); + Assert.Equal("Sunny", output.Weather[0].Condition.Data); + } + + [Fact] + public void Can_Deserialize_Boolean_From_Number() { + var xmlFilePath = PathFor("boolean_from_number.xml"); + var doc = XDocument.Load(xmlFilePath); + var response = new RestResponse { Content = doc.ToString() }; + var d = new XmlAttributeDeserializer(); + var output = d.Deserialize(response)!; + + Assert.True(output.Value); + } + + [Fact] + public void Can_Deserialize_Boolean_From_String() { + var xmlFilePath = PathFor("boolean_from_string.xml"); + var doc = XDocument.Load(xmlFilePath); + var response = new RestResponse { Content = doc.ToString() }; + var d = new XmlAttributeDeserializer(); + var output = d.Deserialize(response)!; + + Assert.True(output.Value); + } + + [Fact] + public void Can_Deserialize_Empty_Elements_With_Attributes_to_Nullable_Values() { + var doc = CreateXmlWithAttributesAndNullValues(); + var xml = new XmlAttributeDeserializer(); + var output = xml.Deserialize(new() { Content = doc })!; + + Assert.Null(output.Id); + Assert.Null(output.StartDate); + Assert.Null(output.UniqueId); + } + + [Fact] + public void Can_Deserialize_Mixture_Of_Empty_Elements_With_Attributes_And_Populated_Elements() { + var doc = CreateXmlWithAttributesAndNullValuesAndPopulatedValues(); + var xml = new XmlAttributeDeserializer(); + var output = xml.Deserialize(new RestResponse { Content = doc })!; + + Assert.Null(output.Id); + Assert.Null(output.StartDate); + Assert.Equal(new Guid(GuidString), output.UniqueId); + } + + [Fact] + public void Can_Deserialize_DateTimeOffset() { + var culture = CultureInfo.InvariantCulture; + var doc = new XDocument(culture); + + var dateTimeOffset = new DateTimeOffset(2013, 02, 08, 9, 18, 22, TimeSpan.FromHours(10)); + + DateTimeOffset? nullableDateTimeOffsetWithValue = + new DateTimeOffset(2013, 02, 08, 9, 18, 23, TimeSpan.FromHours(10)); + + var root = new XElement("Dates"); + + root.Add(new XElement("DateTimeOffset", dateTimeOffset)); + root.Add(new XElement("NullableDateTimeOffsetWithNull", string.Empty)); + root.Add(new XElement("NullableDateTimeOffsetWithValue", nullableDateTimeOffsetWithValue)); + + doc.Add(root); + + //var xml = new XmlAttributeDeserializer { Culture = culture }; + var response = new RestResponse { Content = doc.ToString() }; + var d = new XmlAttributeDeserializer { Culture = culture }; + var payload = d.Deserialize(response)!; + + Assert.Equal(dateTimeOffset, payload.DateTimeOffset); + Assert.Null(payload.NullableDateTimeOffsetWithNull); + Assert.True(payload.NullableDateTimeOffsetWithValue.HasValue); + Assert.Equal(nullableDateTimeOffsetWithValue, payload.NullableDateTimeOffsetWithValue); + } + + static string CreateUnderscoresXml() { + var doc = new XDocument(); + var root = new XElement("Person"); + + root.Add(new XElement("Name", "John Sheehan")); + root.Add(new XElement("Start_Date", new DateTime(2009, 9, 25, 0, 6, 1))); + root.Add(new XAttribute("Age", 28)); + root.Add(new XElement("Percent", 99.9999m)); + root.Add(new XElement("Big_Number", long.MaxValue)); + root.Add(new XAttribute("Is_Cool", false)); + root.Add(new XElement("Ignore", "dummy")); + root.Add(new XAttribute("Read_Only", "dummy")); + root.Add(new XElement("Unique_Id", new Guid(GuidString))); + root.Add(new XElement("Url", "http://example.com")); + root.Add(new XElement("Url_Path", "/foo/bar")); + + root.Add( + new XElement( + "Best_Friend", + new XElement("Name", "The Fonz"), + new XAttribute("Since", 1952) + ) + ); + + var friends = new XElement("Friends"); + + for (var i = 0; i < 10; i++) { + friends.Add( + new XElement( + "Friend", + new XElement("Name", "Friend" + i), + new XAttribute("Since", DateTime.Now.Year - i) + ) + ); + } + + root.Add(friends); + + var foes = new XElement("Foes"); + + foes.Add(new XAttribute("Team", "Yankees")); + + for (var i = 0; i < 5; i++) foes.Add(new XElement("Foe", new XElement("Nickname", "Foe" + i))); + + root.Add(foes); + doc.Add(root); + + return doc.ToString(); + } + + static string CreateLowercaseUnderscoresXml() { + var doc = new XDocument(); + var root = new XElement("Person"); + + root.Add(new XElement("Name", "John Sheehan")); + root.Add(new XElement("start_date", new DateTime(2009, 9, 25, 0, 6, 1))); + root.Add(new XAttribute("Age", 28)); + root.Add(new XElement("Percent", 99.9999m)); + root.Add(new XElement("big_number", long.MaxValue)); + root.Add(new XAttribute("is_cool", false)); + root.Add(new XElement("Ignore", "dummy")); + root.Add(new XAttribute("read_only", "dummy")); + root.Add(new XElement("unique_id", new Guid(GuidString))); + root.Add(new XElement("Url", "http://example.com")); + root.Add(new XElement("url_path", "/foo/bar")); + + root.Add( + new XElement( + "best_friend", + new XElement("name", "The Fonz"), + new XAttribute("Since", 1952) + ) + ); + + var friends = new XElement("Friends"); + + for (var i = 0; i < 10; i++) { + friends.Add( + new XElement( + "Friend", + new XElement("Name", "Friend" + i), + new XAttribute("Since", DateTime.Now.Year - i) + ) + ); + } + + root.Add(friends); + + var foes = new XElement("Foes"); + + foes.Add(new XAttribute("Team", "Yankees")); + + for (var i = 0; i < 5; i++) foes.Add(new XElement("Foe", new XElement("Nickname", "Foe" + i))); + + root.Add(foes); + doc.Add(root); + + return doc.ToString(); + } + + static string CreateDashesXml() { + var doc = new XDocument(); + var root = new XElement("Person"); + + root.Add(new XElement("Name", "John Sheehan")); + root.Add(new XElement("Start_Date", new DateTime(2009, 9, 25, 0, 6, 1))); + root.Add(new XAttribute("Age", 28)); + root.Add(new XElement("Percent", 99.9999m)); + root.Add(new XElement("Big-Number", long.MaxValue)); + root.Add(new XAttribute("Is-Cool", false)); + root.Add(new XElement("Ignore", "dummy")); + root.Add(new XAttribute("Read-Only", "dummy")); + root.Add(new XElement("Unique-Id", new Guid(GuidString))); + root.Add(new XElement("Url", "http://example.com")); + root.Add(new XElement("Url-Path", "/foo/bar")); + + root.Add( + new XElement( + "Best-Friend", + new XElement("Name", "The Fonz"), + new XAttribute("Since", 1952) + ) + ); + + var friends = new XElement("Friends"); + + for (var i = 0; i < 10; i++) { + friends.Add( + new XElement( + "Friend", + new XElement("Name", "Friend" + i), + new XAttribute("Since", DateTime.Now.Year - i) + ) + ); + } + + root.Add(friends); + + var foes = new XElement("Foes"); + + foes.Add(new XAttribute("Team", "Yankees")); + + for (var i = 0; i < 5; i++) foes.Add(new XElement("Foe", new XElement("Nickname", "Foe" + i))); + + root.Add(foes); + doc.Add(root); + + return doc.ToString(); + } + + static string CreateLowerCasedRootElementWithDashesXml() { + var doc = new XDocument(); + + var root = new XElement( + "incoming-invoices", + new XElement( + "incoming-invoice", + new XElement("concept-id", 45) + ) + ); + + doc.Add(root); + + return doc.ToString(); + } + + static string CreateElementsXml() { + var doc = new XDocument(); + var root = new XElement("Person"); + + root.Add(new XElement("Name", "John Sheehan")); + root.Add(new XElement("StartDate", new DateTime(2009, 9, 25, 0, 6, 1))); + root.Add(new XElement("Age", 28)); + root.Add(new XElement("Percent", 99.9999m)); + root.Add(new XElement("BigNumber", long.MaxValue)); + root.Add(new XElement("IsCool", false)); + root.Add(new XElement("Ignore", "dummy")); + root.Add(new XElement("ReadOnly", "dummy")); + root.Add(new XElement("UniqueId", new Guid(GuidString))); + root.Add(new XElement("EmptyGuid", "")); + root.Add(new XElement("Url", "http://example.com")); + root.Add(new XElement("UrlPath", "/foo/bar")); + root.Add(new XElement("Order", "third")); + root.Add(new XElement("Disposition", "so-so")); + + root.Add( + new XElement( + "BestFriend", + new XElement("Name", "The Fonz"), + new XElement("Since", 1952) + ) + ); + + var friends = new XElement("Friends"); + + for (var i = 0; i < 10; i++) { + friends.Add( + new XElement( + "Friend", + new XElement("Name", "Friend" + i), + new XElement("Since", DateTime.Now.Year - i) + ) + ); + } + + root.Add(friends); + + root.Add( + new XElement( + "FavoriteBand", + new XElement("Name", "Goldfinger") + ) + ); + + doc.Add(root); + + return doc.ToString(); + } + + static string CreateAttributesXml() { + var doc = new XDocument(); + var root = new XElement("Person"); + + root.Add(new XAttribute("Name", "John Sheehan")); + root.Add(new XAttribute("StartDate", new DateTime(2009, 9, 25, 0, 6, 1))); + root.Add(new XAttribute("Age", 28)); + root.Add(new XAttribute("Percent", 99.9999m)); + root.Add(new XAttribute("BigNumber", long.MaxValue)); + root.Add(new XAttribute("IsCool", false)); + root.Add(new XAttribute("Ignore", "dummy")); + root.Add(new XAttribute("ReadOnly", "dummy")); + root.Add(new XAttribute("UniqueId", new Guid(GuidString))); + root.Add(new XAttribute("Url", "http://example.com")); + root.Add(new XAttribute("UrlPath", "/foo/bar")); + + root.Add( + new XElement( + "BestFriend", + new XAttribute("Name", "The Fonz"), + new XAttribute("Since", 1952) + ) + ); + + doc.Add(root); + + return doc.ToString(); + } + + static string CreateXmlWithNullValues() { + var doc = new XDocument(); + var root = new XElement("NullableValues"); + + root.Add( + new XElement("Id", null), + new XElement("StartDate", null), + new XElement("UniqueId", null) + ); + + doc.Add(root); + + return doc.ToString(); + } + + static string CreateXmlWithoutEmptyValues(CultureInfo culture) { + var doc = new XDocument(); + var root = new XElement("NullableValues"); + + root.Add( + new XElement("Id", 123), + new XElement("StartDate", new DateTime(2010, 2, 21, 9, 35, 00).ToString(culture)), + new XElement("UniqueId", new Guid(GuidString)) + ); + + doc.Add(root); + + return doc.ToString(); + } + + static string CreateXmlWithEmptyNestedList() { + var doc = new XDocument(); + var root = new XElement("EmptyListSample"); + + root.Add(new XElement("Images")); + doc.Add(root); + + return doc.ToString(); + } + + static string CreateXmlWithEmptyInlineList() { + var doc = new XDocument(); + var root = new XElement("EmptyListSample"); + + doc.Add(root); + + return doc.ToString(); + } + + static string CreateXmlWithAttributesAndNullValues() { + var doc = new XDocument(); + var root = new XElement("NullableValues"); + var idElement = new XElement("Id", null); + + idElement.SetAttributeValue("SomeAttribute", "SomeAttribute_Value"); + + root.Add( + idElement, + new XElement("StartDate", null), + new XElement("UniqueId", null) + ); + + doc.Add(root); + + return doc.ToString(); + } + + static string CreateXmlWithAttributesAndNullValuesAndPopulatedValues() { + var doc = new XDocument(); + var root = new XElement("NullableValues"); + var idElement = new XElement("Id", null); + + idElement.SetAttributeValue("SomeAttribute", "SomeAttribute_Value"); + + root.Add( + idElement, + new XElement("StartDate", null), + new XElement("UniqueId", new Guid(GuidString)) + ); + + doc.Add(root); + + return doc.ToString(); + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Serializers.Xml/XmlDeserializerTests.cs b/test/RestSharp.Tests.Serializers.Xml/XmlDeserializerTests.cs new file mode 100644 index 000000000..1c3454356 --- /dev/null +++ b/test/RestSharp.Tests.Serializers.Xml/XmlDeserializerTests.cs @@ -0,0 +1,1243 @@ +using System.Globalization; +using System.Xml.Linq; +using RestSharp.Serializers.Xml; +using RestSharp.Tests.Serializers.Xml.SampleClasses; +using RestSharp.Tests.Serializers.Xml.SampleClasses.DeserializeAsTest; + +namespace RestSharp.Tests.Serializers.Xml; + +public class XmlDeserializerTests { + const string GuidString = "AC1FC4BC-087A-4242-B8EE-C53EBE9887A5"; + +#if NET + readonly string _sampleDataPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "SampleData"); +#else + readonly string _sampleDataPath = Path.Combine(Directory.GetCurrentDirectory(), "SampleData"); +#endif + + string PathFor(string sampleFile) => Path.Combine(_sampleDataPath, sampleFile); + + static string CreateUnderscoresXml() { + var doc = new XDocument(); + var root = new XElement("Person"); + + root.Add(new XElement("Name", "John Sheehan")); + root.Add(new XElement("Start_Date", new DateTime(2009, 9, 25, 0, 6, 1))); + root.Add(new XAttribute("Age", 28)); + root.Add(new XElement("Percent", 99.9999m)); + root.Add(new XElement("Big_Number", long.MaxValue)); + root.Add(new XAttribute("Is_Cool", false)); + root.Add(new XElement("Ignore", "dummy")); + root.Add(new XAttribute("Read_Only", "dummy")); + root.Add(new XElement("Unique_Id", new Guid(GuidString))); + root.Add(new XElement("Url", "https://example.com")); + root.Add(new XElement("Url_Path", "/foo/bar")); + + root.Add( + new XElement( + "Best_Friend", + new XElement("Name", "The Fonz"), + new XAttribute("Since", 1952) + ) + ); + + var friends = new XElement("Friends"); + + for (var i = 0; i < 10; i++) { + friends.Add( + new XElement( + "Friend", + new XElement("Name", $"Friend{i}"), + new XAttribute("Since", DateTime.Now.Year - i) + ) + ); + } + + root.Add(friends); + + var foes = new XElement("Foes"); + + foes.Add(new XAttribute("Team", "Yankees")); + + for (var i = 0; i < 5; i++) foes.Add(new XElement("Foe", new XElement("Nickname", $"Foe{i}"))); + + root.Add(foes); + doc.Add(root); + + return doc.ToString(); + } + + static string CreateLowercaseUnderscoresXml() { + var doc = new XDocument(); + var root = new XElement("Person"); + + root.Add(new XElement("Name", "John Sheehan")); + root.Add(new XElement("start_date", new DateTime(2009, 9, 25, 0, 6, 1))); + root.Add(new XAttribute("Age", 28)); + root.Add(new XElement("Percent", 99.9999m)); + root.Add(new XElement("big_number", long.MaxValue)); + root.Add(new XAttribute("is_cool", false)); + root.Add(new XElement("Ignore", "dummy")); + root.Add(new XAttribute("read_only", "dummy")); + root.Add(new XElement("unique_id", new Guid(GuidString))); + root.Add(new XElement("Url", "https://example.com")); + root.Add(new XElement("url_path", "/foo/bar")); + + root.Add( + new XElement( + "best_friend", + new XElement("name", "The Fonz"), + new XAttribute("Since", 1952) + ) + ); + + var friends = new XElement("Friends"); + + for (var i = 0; i < 10; i++) { + friends.Add( + new XElement( + "Friend", + new XElement("Name", $"Friend{i}"), + new XAttribute("Since", DateTime.Now.Year - i) + ) + ); + } + + root.Add(friends); + + var foes = new XElement("Foes"); + + foes.Add(new XAttribute("Team", "Yankees")); + + for (var i = 0; i < 5; i++) foes.Add(new XElement("Foe", new XElement("Nickname", $"Foe{i}"))); + + root.Add(foes); + doc.Add(root); + + return doc.ToString(); + } + + static string CreateDashesXml() { + var doc = new XDocument(); + var root = new XElement("Person"); + + root.Add(new XElement("Name", "John Sheehan")); + root.Add(new XElement("Start_Date", new DateTime(2009, 9, 25, 0, 6, 1))); + root.Add(new XAttribute("Age", 28)); + root.Add(new XElement("Percent", 99.9999m)); + root.Add(new XElement("Big-Number", long.MaxValue)); + root.Add(new XAttribute("Is-Cool", false)); + root.Add(new XElement("Ignore", "dummy")); + root.Add(new XAttribute("Read-Only", "dummy")); + root.Add(new XElement("Unique-Id", new Guid(GuidString))); + root.Add(new XElement("Url", "https://example.com")); + root.Add(new XElement("Url-Path", "/foo/bar")); + + root.Add( + new XElement( + "Best-Friend", + new XElement("Name", "The Fonz"), + new XAttribute("Since", 1952) + ) + ); + + var friends = new XElement("Friends"); + + for (var i = 0; i < 10; i++) { + friends.Add( + new XElement( + "Friend", + new XElement("Name", $"Friend{i}"), + new XAttribute("Since", DateTime.Now.Year - i) + ) + ); + } + + root.Add(friends); + + var foes = new XElement("Foes"); + + foes.Add(new XAttribute("Team", "Yankees")); + + for (var i = 0; i < 5; i++) foes.Add(new XElement("Foe", new XElement("Nickname", $"Foe{i}"))); + + root.Add(foes); + doc.Add(root); + + return doc.ToString(); + } + + static string CreateLowerCasedRootElementWithDashesXml() { + var doc = new XDocument(); + + var root = new XElement( + "incoming-invoices", + new XElement("incoming-invoice", new XElement("concept-id", 45)) + ); + + doc.Add(root); + + return doc.ToString(); + } + + static string CreateElementsXml() { + var doc = new XDocument(); + var root = new XElement("Person"); + + root.Add(new XElement("Name", "John Sheehan")); + root.Add(new XElement("StartDate", new DateTime(2009, 9, 25, 0, 6, 1))); + root.Add(new XElement("Age", 28)); + root.Add(new XElement("Percent", 99.9999m)); + root.Add(new XElement("BigNumber", long.MaxValue)); + root.Add(new XElement("IsCool", false)); + root.Add(new XElement("Ignore", "dummy")); + root.Add(new XElement("ReadOnly", "dummy")); + root.Add(new XElement("UniqueId", new Guid(GuidString))); + root.Add(new XElement("EmptyGuid", "")); + root.Add(new XElement("Url", "https://example.com")); + root.Add(new XElement("UrlPath", "/foo/bar")); + root.Add(new XElement("Order", "third")); + root.Add(new XElement("Disposition", "so-so")); + + root.Add( + new XElement( + "BestFriend", + new XElement("Name", "The Fonz"), + new XElement("Since", 1952) + ) + ); + + var friends = new XElement("Friends"); + + for (var i = 0; i < 10; i++) { + friends.Add( + new XElement( + "Friend", + new XElement("Name", $"Friend{i}"), + new XElement("Since", DateTime.Now.Year - i) + ) + ); + } + + root.Add(friends); + doc.Add(root); + + return doc.ToString(); + } + + static string CreateAttributesXml() { + var doc = new XDocument(); + var root = new XElement("Person"); + + root.Add(new XAttribute("Name", "John Sheehan")); + root.Add(new XAttribute("StartDate", new DateTime(2009, 9, 25, 0, 6, 1))); + root.Add(new XAttribute("Age", 28)); + root.Add(new XAttribute("Percent", 99.9999m)); + root.Add(new XAttribute("BigNumber", long.MaxValue)); + root.Add(new XAttribute("IsCool", false)); + root.Add(new XAttribute("Ignore", "dummy")); + root.Add(new XAttribute("ReadOnly", "dummy")); + root.Add(new XAttribute("UniqueId", new Guid(GuidString))); + root.Add(new XAttribute("Url", "https://example.com")); + root.Add(new XAttribute("UrlPath", "/foo/bar")); + + root.Add( + new XElement( + "BestFriend", + new XAttribute("Name", "The Fonz"), + new XAttribute("Since", 1952) + ) + ); + + doc.Add(root); + + return doc.ToString(); + } + + static string CreateNoteXml() { + var doc = new XDocument(); + var root = new XElement("Note"); + + root.SetAttributeValue("Id", 1); + root.Value = Note.ConstMessage; + root.Add(new XElement("Title", Note.ConstTitle)); + + doc.Add(root); + + return doc.ToString(); + } + + static string CreateXmlWithNullValues() { + var doc = new XDocument(); + var root = new XElement("NullableValues"); + + root.Add( + new XElement("Id", null!), + new XElement("StartDate", null!), + new XElement("UniqueId", null!) + ); + + doc.Add(root); + + return doc.ToString(); + } + + static string CreateXmlWithoutEmptyValues(CultureInfo culture) { + var doc = new XDocument(); + var root = new XElement("NullableValues"); + + root.Add( + new XElement("Id", 123), + new XElement("StartDate", new DateTime(2010, 2, 21, 9, 35, 00).ToString(culture)), + new XElement("UniqueId", new Guid(GuidString)) + ); + + doc.Add(root); + + return doc.ToString(); + } + + static string CreateXmlWithEmptyNestedList() { + var doc = new XDocument(); + var root = new XElement("EmptyListSample"); + + root.Add(new XElement("Images")); + doc.Add(root); + + return doc.ToString(); + } + + static string CreateXmlWithEmptyInlineList() { + var doc = new XDocument(); + var root = new XElement("EmptyListSample"); + + doc.Add(root); + + return doc.ToString(); + } + + static string CreateXmlWithAttributesAndNullValues() { + var doc = new XDocument(); + var root = new XElement("NullableValues"); + var idElement = new XElement("Id", null!); + + idElement.SetAttributeValue("SomeAttribute", "SomeAttribute_Value"); + + root.Add( + idElement, + new XElement("StartDate", null!), + new XElement("UniqueId", null!) + ); + + doc.Add(root); + + return doc.ToString(); + } + + static string CreateXmlWithAttributesAndNullValuesAndPopulatedValues() { + var doc = new XDocument(); + var root = new XElement("NullableValues"); + var idElement = new XElement("Id", null!); + + idElement.SetAttributeValue("SomeAttribute", "SomeAttribute_Value"); + + root.Add( + idElement, + new XElement("StartDate", null!), + new XElement("UniqueId", new Guid(GuidString)) + ); + + doc.Add(root); + + return doc.ToString(); + } + + [Fact] + public void Able_to_use_alternative_name_for_arrays() { + var xmlFilePath = PathFor("header_and_rows.xml"); + var doc = XDocument.Load(xmlFilePath); + var xml = new XmlDeserializer(); + var output = xml.Deserialize
(new() { Content = doc.ToString() })!; + + Assert.NotNull(output); + Assert.Equal("text title", output.Title); + Assert.Contains(output.Othername, x => x.Text1 == "first row text 1 sample"); + } + + [Fact] + public void Can_deal_with_value_attribute() { + const string content = "Green255"; + var xml = new XmlDeserializer(); + var output = xml.Deserialize(new() { Content = content })!; + + Assert.NotNull(output); + Assert.Equal("Green", output.Name); + Assert.Equal(255, output.Value); + } + + [Fact] + public void Can_Deserialize_Attribute_Using_Exact_Name_Defined_In_DeserializeAs_Attribute() { + const string content = """"""; + + var expected = new NodeWithAttributeAndValue { + AttributeValue = "711" + }; + + var xml = new XmlDeserializer(); + var output = xml.Deserialize(new() { Content = content })!; + + Assert.Equal(expected.AttributeValue, output.AttributeValue); + } + + [Fact] + public void Can_Deserialize_AttributeNamedValue() { + var doc = new XDocument(); + var root = new XElement("ValueCollection"); + + var xmlCollection = new XElement("Values"); + + var first = new XElement("Value"); + first.Add(new XAttribute("Timestamp", new DateTime(1969, 7, 20, 20, 18, 00, DateTimeKind.Utc))); + first.Add(new XAttribute("Value", "Eagle landed")); + + xmlCollection.Add(first); + + var second = new XElement("Value"); + second.Add(new XAttribute("Timestamp", new DateTime(1969, 7, 21, 2, 56, 15, DateTimeKind.Utc))); + second.Add(new XAttribute("Value", "First step")); + xmlCollection.Add(second); + + root.Add(xmlCollection); + doc.Add(root); + + var response = new RestResponse { Content = doc.ToString() }; + var d = new XmlDeserializer(); + var valueCollection = d.Deserialize(response)!; + + Assert.Equal(2, valueCollection.Values.Count); + Assert.Equal("Eagle landed", valueCollection.Values.First().Value); + } + + [Fact] + public void Can_Deserialize_Attributes_On_Default_Root() { + var doc = CreateAttributesXml(); + var response = new RestResponse { Content = doc }; + var d = new XmlDeserializer(); + var p = d.Deserialize(response)!; + + Assert.Equal("John Sheehan", p.Name); + Assert.Equal(new DateTime(2009, 9, 25, 0, 6, 1), p.StartDate); + Assert.Equal(28, p.Age); + Assert.Equal(long.MaxValue, p.BigNumber); + Assert.Equal(99.9999m, p.Percent); + Assert.False(p.IsCool); + Assert.Equal(new Guid(GuidString), p.UniqueId); + Assert.Equal(new Uri("https://example.com", UriKind.RelativeOrAbsolute), p.Url); + Assert.Equal(new Uri("/foo/bar", UriKind.RelativeOrAbsolute), p.UrlPath); + Assert.NotNull(p.BestFriend); + Assert.Equal("The Fonz", p.BestFriend.Name); + Assert.Equal(1952, p.BestFriend.Since); + } + + [Fact] + public void Can_Deserialize_Boolean_From_Number() { + var xmlFilePath = PathFor("boolean_from_number.xml"); + var doc = XDocument.Load(xmlFilePath); + var response = new RestResponse { Content = doc.ToString() }; + var d = new XmlDeserializer(); + var output = d.Deserialize(response)!; + + Assert.True(output.Value); + } + + [Fact] + public void Can_Deserialize_Boolean_From_String() { + var xmlFilePath = PathFor("boolean_from_string.xml"); + var doc = XDocument.Load(xmlFilePath); + var response = new RestResponse { Content = doc.ToString() }; + var d = new XmlDeserializer(); + var output = d.Deserialize(response)!; + + Assert.True(output.Value); + } + + [Fact] + public void Can_Deserialize_Custom_Formatted_Date() { + const string format = "dd yyyy MMM, hh:mm ss tt zzz"; + + var culture = CultureInfo.InvariantCulture; + var date = new DateTime(2010, 2, 8, 11, 11, 11); + var doc = new XDocument(); + var root = new XElement("Person"); + + root.Add(new XElement("StartDate", date.ToString(format, culture))); + doc.Add(root); + + var xml = new XmlDeserializer { + DateFormat = format, + Culture = culture + }; + var response = new RestResponse { Content = doc.ToString() }; + var output = xml.Deserialize(response)!; + + Assert.Equal(date, output.StartDate); + } + + [Fact] + public void Can_Deserialize_DateTimeOffset() { + var culture = CultureInfo.InvariantCulture; + var doc = new XDocument(culture); + var dateTimeOffset = new DateTimeOffset(2013, 02, 08, 9, 18, 22, TimeSpan.FromHours(10)); + + DateTimeOffset? nullableDateTimeOffsetWithValue = + new DateTimeOffset(2013, 02, 08, 9, 18, 23, TimeSpan.FromHours(10)); + var root = new XElement("Dates"); + + root.Add(new XElement("DateTimeOffset", dateTimeOffset)); + root.Add(new XElement("NullableDateTimeOffsetWithNull", string.Empty)); + root.Add(new XElement("NullableDateTimeOffsetWithValue", nullableDateTimeOffsetWithValue)); + + doc.Add(root); + + //var xml = new XmlDeserializer { Culture = culture, }; + var response = new RestResponse { Content = doc.ToString() }; + var d = new XmlDeserializer { Culture = culture }; + var payload = d.Deserialize(response)!; + + Assert.Equal(dateTimeOffset, payload.DateTimeOffset); + Assert.Null(payload.NullableDateTimeOffsetWithNull); + Assert.True(payload.NullableDateTimeOffsetWithValue.HasValue); + Assert.Equal(nullableDateTimeOffsetWithValue, payload.NullableDateTimeOffsetWithValue); + } + + [Fact] + public void Can_Deserialize_Directly_To_Lists_Off_Root_Element() { + var xmlFilePath = PathFor("directlists.xml"); + var doc = XDocument.Load(xmlFilePath); + var xml = new XmlDeserializer(); + var output = xml.Deserialize>(new() { Content = doc.ToString() })!; + + Assert.Equal(2, output.Count); + } + + [Fact] + public void Can_Deserialize_ElementNamedValue() { + var doc = new XDocument(); + var root = new XElement("ValueCollection"); + + const string valueName = "First moon landing events"; + root.Add(new XElement("Value", valueName)); + + var xmlCollection = new XElement("Values"); + + var first = new XElement("Value"); + first.Add(new XAttribute("Timestamp", new DateTime(1969, 7, 20, 20, 18, 00, DateTimeKind.Utc))); + xmlCollection.Add(first); + + var second = new XElement("Value"); + second.Add(new XAttribute("Timestamp", new DateTime(1969, 7, 21, 2, 56, 15, DateTimeKind.Utc))); + xmlCollection.Add(second); + + root.Add(xmlCollection); + doc.Add(root); + + var response = new RestResponse { Content = doc.ToString() }; + var d = new XmlDeserializer(); + var valueCollection = d.Deserialize(response)!; + + Assert.Equal(valueName, valueCollection.Value); + Assert.Equal(2, valueCollection.Values.Count); + + Assert.Equal( + new DateTime(1969, 7, 20, 20, 18, 00, DateTimeKind.Utc), + valueCollection.Values.First().Timestamp.ToUniversalTime() + ); + } + + [Fact] + public void Can_Deserialize_Elements_On_Default_Root() { + var doc = CreateElementsXml(); + var response = new RestResponse { Content = doc }; + var d = new XmlDeserializer(); + var p = d.Deserialize(response)!; + + Assert.Equal("John Sheehan", p.Name); + Assert.Equal(new DateTime(2009, 9, 25, 0, 6, 1), p.StartDate); + Assert.Equal(28, p.Age); + Assert.Equal(long.MaxValue, p.BigNumber); + Assert.Equal(99.9999m, p.Percent); + Assert.False(p.IsCool); + Assert.Equal(new Guid(GuidString), p.UniqueId); + Assert.Equal(Guid.Empty, p.EmptyGuid); + Assert.Equal(new Uri("https://example.com", UriKind.RelativeOrAbsolute), p.Url); + Assert.Equal(new Uri("/foo/bar", UriKind.RelativeOrAbsolute), p.UrlPath); + Assert.Equal(Order.Third, p.Order); + Assert.Equal(Disposition.SoSo, p.Disposition); + Assert.NotNull(p.Friends); + Assert.Equal(10, p.Friends.Count); + Assert.NotNull(p.BestFriend); + Assert.Equal("The Fonz", p.BestFriend.Name); + Assert.Equal(1952, p.BestFriend.Since); + } + + [Fact] + public void Can_Deserialize_Elements_to_Nullable_Values() { + var culture = CultureInfo.InvariantCulture; + var doc = CreateXmlWithoutEmptyValues(culture); + + var xml = new XmlDeserializer { + Culture = culture + }; + var output = xml.Deserialize(new() { Content = doc })!; + + Assert.NotNull(output.Id); + Assert.NotNull(output.StartDate); + Assert.NotNull(output.UniqueId); + Assert.Equal(123, output.Id); + Assert.Equal(new DateTime(2010, 2, 21, 9, 35, 00), output.StartDate); + Assert.Equal(new Guid(GuidString), output.UniqueId); + } + + [Fact] + public void Can_Deserialize_Empty_Elements_to_Nullable_Values() { + var doc = CreateXmlWithNullValues(); + var xml = new XmlDeserializer(); + var output = xml.Deserialize(new() { Content = doc })!; + + Assert.Null(output.Id); + Assert.Null(output.StartDate); + Assert.Null(output.UniqueId); + } + + [Fact] + public void Can_Deserialize_Empty_Elements_With_Attributes_to_Nullable_Values() { + var doc = CreateXmlWithAttributesAndNullValues(); + var xml = new XmlDeserializer(); + var output = xml.Deserialize(new() { Content = doc })!; + + Assert.Null(output.Id); + Assert.Null(output.StartDate); + Assert.Null(output.UniqueId); + } + + [Fact] + public void Can_Deserialize_Eventful_Xml() { + var xmlFilePath = PathFor("eventful.xml"); + var doc = XDocument.Load(xmlFilePath); + var response = new RestResponse { Content = doc.ToString() }; + var d = new XmlDeserializer(); + var output = d.Deserialize(response)!; + + Assert.Equal(3, output.venues.Count); + Assert.Equal("Tivoli", output.venues[0].name); + Assert.Equal("https://eventful.com/brisbane/venues/tivoli-/V0-001-002169294-8", output.venues[1].url); + Assert.Equal("V0-001-000266914-3", output.venues[2].id); + } + + [Fact] + public void Can_Deserialize_Goodreads_Xml() { + var xmlFilePath = PathFor("Goodreads.xml"); + var doc = XDocument.Load(xmlFilePath); + var response = new RestResponse { Content = doc.ToString() }; + var d = new XmlDeserializer(); + var output = d.Deserialize(response)!; + + Assert.Equal(2, output.Reviews.Count); + Assert.Equal("1208943892", output.Reviews[0].Id); // This fails without fixing the XmlDeserializer + Assert.Equal("1198344567", output.Reviews[1].Id); + } + + [Fact] + public void Can_throw_format_exception_xml() { + var xmlPath = PathFor("GoodreadsFormatError.xml"); + var doc = XDocument.Load(xmlPath); + var response = new RestResponse { Content = doc.ToString() }; + var d = new XmlDeserializer(); + + Assert.Throws(() => d.Deserialize(response) + ); + } + + [Fact] + public void Can_Deserialize_Google_Weather_Xml() { + var xmlPath = PathFor("GoogleWeather.xml"); + var doc = XDocument.Load(xmlPath); + var response = new RestResponse { Content = doc.ToString() }; + var d = new XmlDeserializer(); + var output = d.Deserialize(response)!; + + Assert.Equal(4, output.weather.Count); + Assert.Equal("Sunny", output.weather[0].condition.data); + } + + [Fact] + public void Can_Deserialize_Inline_List_Without_Elements_To_Empty_List() { + var doc = CreateXmlWithEmptyInlineList(); + var xml = new XmlDeserializer(); + var output = xml.Deserialize(new() { Content = doc })!; + + Assert.NotNull(output.Images); + Assert.Empty(output.Images); + } + + [Fact] + public void Can_Deserialize_Into_Struct() { + const string content = "oneOneOnetwoTwoTwo3"; + + var xml = new XmlDeserializer(); + var output = xml.Deserialize(new() { Content = content }); + + Assert.Equal("oneOneOne", output.One); + Assert.Equal("twoTwoTwo", output.Two); + Assert.Equal(3, output.Three); + } + + [Fact] + public void Can_Deserialize_Lastfm_Xml() { + var xmlPath = PathFor("Lastfm.xml"); + var doc = XDocument.Load(xmlPath); + var response = new RestResponse { Content = doc.ToString() }; + var d = new XmlDeserializer(); + var output = d.Deserialize(response)!; + + Assert.Equal( + "https://www.last.fm/event/328799+Philip+Glass+at+Barbican+Centre+on+12+June+2008", + output.url + ); + Assert.Equal("https://www.last.fm/venue/8777860+Barbican+Centre", output.venue.url); + } + + [Fact] + public void Can_Deserialize_Lists_of_Simple_Types() { + var xmlPath = PathFor("xmllists.xml"); + var doc = XDocument.Load(xmlPath); + var xml = new XmlDeserializer(); + + var output = xml.Deserialize(new() { Content = doc.ToString() })!; + + Assert.NotEqual(0, output.Names[0].Length); + Assert.NotEqual(0, output.Numbers.Sum()); + } + + [Fact] + public void Can_Deserialize_Lower_Cased_Root_Elements_With_Dashes() { + var doc = CreateDashesXml(); + var response = new RestResponse { Content = doc }; + var d = new XmlDeserializer(); + var p = d.Deserialize(response)!; + + Assert.Equal("John Sheehan", p.Name); + Assert.Equal(new(2009, 9, 25, 0, 6, 1), p.StartDate); + Assert.Equal(28, p.Age); + Assert.Equal(long.MaxValue, p.BigNumber); + Assert.Equal(99.9999m, p.Percent); + Assert.False(p.IsCool); + Assert.Equal(new(GuidString), p.UniqueId); + Assert.Equal(new("https://example.com", UriKind.RelativeOrAbsolute), p.Url); + Assert.Equal(new("/foo/bar", UriKind.RelativeOrAbsolute), p.UrlPath); + Assert.NotNull(p.Friends); + Assert.Equal(10, p.Friends.Count); + Assert.NotNull(p.BestFriend); + Assert.Equal("The Fonz", p.BestFriend.Name); + Assert.Equal(1952, p.BestFriend.Since); + Assert.NotNull(p.Foes); + Assert.Equal(5, p.Foes.Count); + Assert.Equal("Yankees", p.Foes.Team); + } + + [Fact] + public void Can_Deserialize_Mixture_Of_Empty_Elements_With_Attributes_And_Populated_Elements() { + var doc = CreateXmlWithAttributesAndNullValuesAndPopulatedValues(); + var xml = new XmlDeserializer(); + var output = xml.Deserialize(new() { Content = doc })!; + + Assert.Null(output.Id); + Assert.Null(output.StartDate); + Assert.Equal(new Guid(GuidString), output.UniqueId); + } + + [Fact] + public void Can_Deserialize_Names_With_Dashes_On_Default_Root() { + var doc = CreateDashesXml(); + var response = new RestResponse { Content = doc }; + var d = new XmlDeserializer(); + var p = d.Deserialize(response)!; + + Assert.Equal("John Sheehan", p.Name); + Assert.Equal(new(2009, 9, 25, 0, 6, 1), p.StartDate); + Assert.Equal(28, p.Age); + Assert.Equal(long.MaxValue, p.BigNumber); + Assert.Equal(99.9999m, p.Percent); + Assert.False(p.IsCool); + Assert.Equal(new(GuidString), p.UniqueId); + Assert.Equal(new("https://example.com", UriKind.RelativeOrAbsolute), p.Url); + Assert.Equal(new("/foo/bar", UriKind.RelativeOrAbsolute), p.UrlPath); + Assert.NotNull(p.Friends); + Assert.Equal(10, p.Friends.Count); + Assert.NotNull(p.BestFriend); + Assert.Equal("The Fonz", p.BestFriend.Name); + Assert.Equal(1952, p.BestFriend.Since); + Assert.NotNull(p.Foes); + Assert.Equal(5, p.Foes.Count); + Assert.Equal("Yankees", p.Foes.Team); + } + + [Fact] + public void Can_Deserialize_Names_With_Underscores_On_Default_Root() { + var doc = CreateUnderscoresXml(); + var response = new RestResponse { Content = doc }; + var d = new XmlDeserializer(); + var p = d.Deserialize(response)!; + + Assert.Equal("John Sheehan", p.Name); + Assert.Equal(new(2009, 9, 25, 0, 6, 1), p.StartDate); + Assert.Equal(28, p.Age); + Assert.Equal(long.MaxValue, p.BigNumber); + Assert.Equal(99.9999m, p.Percent); + Assert.False(p.IsCool); + Assert.Equal(new(GuidString), p.UniqueId); + Assert.Equal(new("https://example.com", UriKind.RelativeOrAbsolute), p.Url); + Assert.Equal(new("/foo/bar", UriKind.RelativeOrAbsolute), p.UrlPath); + Assert.NotNull(p.Friends); + Assert.Equal(10, p.Friends.Count); + Assert.NotNull(p.BestFriend); + Assert.Equal("The Fonz", p.BestFriend.Name); + Assert.Equal(1952, p.BestFriend.Since); + Assert.NotNull(p.Foes); + Assert.Equal(5, p.Foes.Count); + Assert.Equal("Yankees", p.Foes.Team); + } + + [Fact] + public void Can_Deserialize_Names_With_Underscores_Without_Matching_Case_On_Default_Root() { + var doc = CreateLowercaseUnderscoresXml(); + var response = new RestResponse { Content = doc }; + var d = new XmlDeserializer(); + var p = d.Deserialize(response)!; + + Assert.Equal("John Sheehan", p.Name); + Assert.Equal(new(2009, 9, 25, 0, 6, 1), p.StartDate); + Assert.Equal(28, p.Age); + Assert.Equal(long.MaxValue, p.BigNumber); + Assert.Equal(99.9999m, p.Percent); + Assert.False(p.IsCool); + Assert.Equal(new(GuidString), p.UniqueId); + Assert.Equal(new("https://example.com", UriKind.RelativeOrAbsolute), p.Url); + Assert.Equal(new("/foo/bar", UriKind.RelativeOrAbsolute), p.UrlPath); + Assert.NotNull(p.Friends); + Assert.Equal(10, p.Friends.Count); + Assert.NotNull(p.BestFriend); + Assert.Equal("The Fonz", p.BestFriend.Name); + Assert.Equal(1952, p.BestFriend.Since); + Assert.NotNull(p.Foes); + Assert.Equal(5, p.Foes.Count); + Assert.Equal("Yankees", p.Foes.Team); + } + + [Fact] + public void Can_Deserialize_Nested_List_Items_With_Matching_Class_Name() { + var xmlFilePath = PathFor("NestedListSample.xml"); + var doc = XDocument.Load(xmlFilePath); + var xml = new XmlDeserializer(); + var output = xml.Deserialize(new() { Content = doc.ToString() })!; + + Assert.Equal(4, output.images.Count); + } + + [Fact] + public void Can_Deserialize_Nested_List_Items_Without_Matching_Class_Name() { + var xmlFilePath = PathFor("NestedListSample.xml"); + var doc = XDocument.Load(xmlFilePath); + var xml = new XmlDeserializer(); + var output = xml.Deserialize(new() { Content = doc.ToString() })!; + + Assert.Equal(4, output.Images.Count); + } + + [Fact] + public void Can_Deserialize_Nested_List_Without_Elements_To_Empty_List() { + var doc = CreateXmlWithEmptyNestedList(); + var xml = new XmlDeserializer(); + var output = xml.Deserialize(new() { Content = doc })!; + + Assert.NotNull(output.Images); + Assert.Empty(output.Images); + } + + [Fact] + public void Can_Deserialize_Node_That_Has_Attribute_And_Content() { + var doc = CreateNoteXml(); + + var response = new RestResponse { Content = doc }; + var d = new XmlDeserializer(); + + var note = d.Deserialize(response)!; + + Assert.Equal(1, note.Id); + Assert.Equal(Note.ConstTitle, note.Title); + Assert.Equal(Note.ConstMessage, note.Message); + } + + [Fact] + public void Can_Deserialize_Node_Using_Exact_Name_Defined_In_DeserializeAs_Attribute() { + const string content = "711"; + + var expected = new SingleNode { Node = "711" }; + + var xml = new XmlDeserializer(); + var output = xml.Deserialize(new() { Content = content })!; + + Assert.NotNull(output); + Assert.Equal(expected.Node, output.Node); + } + + [Fact] + public void Can_Deserialize_Parentless_aka_Inline_List_Items_With_Matching_Class_Name() { + var xmlFilePath = PathFor("InlineListSample.xml"); + var doc = XDocument.Load(xmlFilePath); + var xml = new XmlDeserializer(); + var output = xml.Deserialize(new() { Content = doc.ToString() })!; + + Assert.Equal(4, output.images.Count); + } + + [Fact] + public void Can_Deserialize_Parentless_aka_Inline_List_Items_With_Matching_Class_Name_With_Additional_Property() { + var xmlFilePath = PathFor("InlineListSample.xml"); + var doc = XDocument.Load(xmlFilePath); + var xml = new XmlDeserializer(); + var output = xml.Deserialize(new() { Content = doc.ToString() })!; + + Assert.Equal(4, output.Count); + } + + [Fact] + public void Can_Deserialize_Parentless_aka_Inline_List_Items_Without_Matching_Class_Name() { + var xmlFilePath = PathFor("InlineListSample.xml"); + var doc = XDocument.Load(xmlFilePath); + var xml = new XmlDeserializer(); + var output = xml.Deserialize(new() { Content = doc.ToString() })!; + + Assert.Equal(4, output.Images.Count); + } + + [Fact] + public void Can_Deserialize_Root_Elements_Without_Matching_Case_And_Dashes() { + var doc = CreateLowerCasedRootElementWithDashesXml(); + var response = new RestResponse { Content = doc }; + var d = new XmlDeserializer(); + var p = d.Deserialize>(response)!; + + Assert.NotNull(p); + Assert.Single(p); + Assert.Equal(45, p[0].ConceptId); + } + + [Fact] + public void Can_Deserialize_TimeSpan() { + var culture = CultureInfo.InvariantCulture; + var doc = new XDocument(culture); + TimeSpan? nullable = new TimeSpan(21, 30, 7); + var root = new XElement("Person"); + + root.Add(new XElement("Tick", new TimeSpan(468006))); + root.Add(new XElement("Millisecond", new TimeSpan(0, 0, 0, 0, 125))); + root.Add(new XElement("Second", new TimeSpan(0, 0, 8))); + root.Add(new XElement("Minute", new TimeSpan(0, 55, 2))); + root.Add(new XElement("Hour", new TimeSpan(21, 30, 7))); + root.Add(new XElement("NullableWithoutValue", (TimeSpan?)null)); + root.Add(new XElement("NullableWithValue", nullable)); + + doc.Add(root); + + var response = new RestResponse { + Content = doc.ToString() + }; + + var d = new XmlDeserializer { Culture = culture }; + var payload = d.Deserialize(response)!; + + Assert.Equal(new(468006), payload.Tick); + Assert.Equal(new(0, 0, 0, 0, 125), payload.Millisecond); + Assert.Equal(new(0, 0, 8), payload.Second); + Assert.Equal(new(0, 55, 2), payload.Minute); + Assert.Equal(new(21, 30, 7), payload.Hour); + Assert.Null(payload.NullableWithoutValue); + Assert.NotNull(payload.NullableWithValue); + Assert.Equal(new(21, 30, 7), payload.NullableWithValue.Value); + } + + [Fact] + public void Can_Deserialize_To_List_Inheritor_From_Custom_Root_With_Attributes() { + var xmlFilePath = PathFor("ListWithAttributes.xml"); + var doc = XDocument.Load(xmlFilePath); + var xml = new XmlDeserializer { RootElement = "Calls" }; + var output = xml.Deserialize(new() { Content = doc.ToString() })!; + + Assert.Equal(3, output.NumPages); + Assert.Equal(2, output.Count); + } + + [Fact] + public void Can_Deserialize_To_Standalone_List_With_Matching_Class_Case() { + var xmlFilePath = PathFor("InlineListSample.xml"); + var doc = XDocument.Load(xmlFilePath); + var xml = new XmlDeserializer(); + var output = xml.Deserialize>(new() { Content = doc.ToString() })!; + + Assert.Equal(4, output.Count); + } + + [Fact] + public void Can_Deserialize_To_Standalone_List_Without_Matching_Class_Case() { + var xmlFilePath = PathFor("InlineListSample.xml"); + var doc = XDocument.Load(xmlFilePath); + var xml = new XmlDeserializer(); + var output = xml.Deserialize>(new() { Content = doc.ToString() })!; + + Assert.Equal(4, output.Count); + } + + [Fact] + public void Can_Deserialize_When_RootElement_Deeper_Then_One() { + const string content = + "oneOneOnetwoTwoTwo3"; + var xml = new XmlDeserializer { RootElement = "subsubroot" }; + var output = xml.Deserialize(new() { Content = content }); + + Assert.Equal("oneOneOne", output.One); + Assert.Equal("twoTwoTwo", output.Two); + Assert.Equal(3, output.Three); + } + + [Fact] + public void Can_Use_DeserializeAs_Attribute() { + const string content = + "1Jacksonoddball"; + var xml = new XmlDeserializer(); + var output = xml.Deserialize(new() { Content = content })!; + + Assert.NotNull(output); + Assert.Equal("1", output.Sid); + Assert.Equal("Jackson", output.FriendlyName); + Assert.Equal("oddball", output.GoodPropertyName); + } + + [Fact] + public void Can_Use_DeserializeAs_Attribute_for_List() { + var xmlFilePath = PathFor("deserialize_as_list.xml"); + var doc = XDocument.Load(xmlFilePath); + var xml = new XmlDeserializer(); + var output = xml.Deserialize>(new() { Content = doc.ToString() })!; + + Assert.NotNull(output); + Assert.Equal("1", output[0].Sid); + } + + [Fact] + public void Can_Use_DeserializeAs_Attribute_for_List_Property() { + const string content = + "TestValue"; + + var xml = new XmlDeserializer(); + var output = xml.Deserialize(new() { Content = content })!; + + Assert.NotNull(output); + Assert.NotNull(output.ListWithGoodName); + Assert.NotEmpty(output.ListWithGoodName); + } + + [Fact] + public void Cannot_Deserialize_Node_To_An_Object_That_Has_Two_Properties_With_Text_Content_Attributes() { + var doc = CreateNoteXml(); + + var response = new RestResponse { Content = doc }; + var d = new XmlDeserializer(); + + Assert.Throws(() => d.Deserialize(response)); + } + + [Fact] + public void Ignore_Protected_Property_That_Exists_In_Data() { + var doc = CreateElementsXml(); + var response = new RestResponse { Content = doc }; + var d = new XmlDeserializer(); + var p = d.Deserialize(response)!; + + Assert.Null(p.IgnoreProxy); + } + + [Fact] + public void Ignore_ReadOnly_Property_That_Exists_In_Data() { + var doc = CreateElementsXml(); + var response = new RestResponse { Content = doc }; + var d = new XmlDeserializer(); + var p = d.Deserialize(response)!; + + Assert.Null(p.ReadOnlyProxy); + } + + [Fact] + public void Deserialize_Nested_List_Should_Not_Include_Deeply_Nested_Items() { + // Bug #1 test: HandleListDerivative should use Elements() on containers, not Descendants() + const string xml = """ + + + + 1 + + 2 + 3 + + + + + """; + + var deserializer = new XmlDeserializer(); + var result = deserializer.Deserialize(new RestResponse { Content = xml })!; + + // Should only have 1 item (id=1), not 3 (id=1,2,3) + Assert.NotNull(result); + Assert.NotNull(result.Items); + Assert.Single(result.Items); + Assert.Equal(1, result.Items[0].Id); + } + + [Fact] + public void Deserialize_RootElement_Should_Not_Throw_On_Duplicate_Nested_Names() { + // Bug #2 test: RootElement selection should handle duplicate names gracefully + const string xml = """ + + + + 72 + + + 74 + + + + + + """; + + var deserializer = new XmlDeserializer { RootElement = "items" }; + + // Should not throw InvalidOperationException: Sequence contains more than one element + var exception = Record.Exception(() => + deserializer.Deserialize(new RestResponse { Content = xml }) + ); + + Assert.Null(exception); + } + + [Fact] + public void Deserialize_RootElement_Should_Prefer_Shallowest_Match() { + // Bug #2 test: When multiple elements match RootElement, prefer the shallowest one + const string xml = """ + + + + 72 + + + 74 + + + + + + """; + + var deserializer = new XmlDeserializer { RootElement = "items" }; + var result = deserializer.Deserialize(new RestResponse { Content = xml })!; + + // Should deserialize from the top-level , not the nested one + Assert.NotNull(result); + Assert.NotNull(result.Items); + Assert.Single(result.Items); + Assert.Equal(72, result.Items[0].Id); + } + + [Fact] + public void Deserialize_RootElement_Should_Try_Direct_Child_First() { + // Bug #2 test: Should try Element() before DescendantsAndSelf() + const string xml = """ + + + direct + + + """; + + var deserializer = new XmlDeserializer { RootElement = "data" }; + var result = deserializer.Deserialize(new RestResponse { Content = xml }); + + Assert.Equal("direct", result.One); + } + + [Fact] + public void Deserialize_List_With_Container_Should_Use_Direct_Children_Only() { + // Test that container-based list deserialization uses Elements() not Descendants() + const string xml = """ + + + + 10 + + 20 + + + + 11 + + + + """; + + var deserializer = new XmlDeserializer(); + var result = deserializer.Deserialize(new RestResponse { Content = xml })!; + + Assert.NotNull(result.Items); + Assert.Equal(2, result.Items.Count); + Assert.Equal(10, result.Items[0].Id); + Assert.Equal(11, result.Items[1].Id); + } + + [Fact] + public void Deserialize_Nested_Items_Should_Also_Use_Direct_Children() { + // Test that nested subitems also correctly use direct children + const string xml = """ + + + + 1 + + 2 + + 3 + + 4 + + + + + + + """; + + var deserializer = new XmlDeserializer(); + var result = deserializer.Deserialize(new RestResponse { Content = xml })!; + + Assert.NotNull(result.Items); + Assert.Single(result.Items); + + var topItem = result.Items[0]; + Assert.Equal(1, topItem.Id); + Assert.NotNull(topItem.SubItems); + Assert.Equal(2, topItem.SubItems.Count); + Assert.Equal(2, topItem.SubItems[0].Id); + Assert.Equal(3, topItem.SubItems[1].Id); + } +} diff --git a/test/RestSharp.Tests.Serializers.Xml/XmlSerializerTests.cs b/test/RestSharp.Tests.Serializers.Xml/XmlSerializerTests.cs new file mode 100644 index 000000000..ae6d6ea2b --- /dev/null +++ b/test/RestSharp.Tests.Serializers.Xml/XmlSerializerTests.cs @@ -0,0 +1,560 @@ +using System.Globalization; +using System.Xml.Linq; +using RestSharp.Serializers; +using RestSharp.Serializers.Xml; +using RestSharp.Tests.Serializers.Xml.SampleClasses; + +namespace RestSharp.Tests.Serializers.Xml; + +public class XmlSerializerTests { + public XmlSerializerTests() { + Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture; + Thread.CurrentThread.CurrentUICulture = CultureInfo.InstalledUICulture; + } + + [Fact] + public void Can_serialize_a_list_of_items_with_interface_type() { + var items = new NamedItems { + Items = [ + new Person { + Name = "Foo", + Age = 50, + Price = 19.95m, + StartDate = new(2009, 12, 18, 10, 2, 23), + Items = [new() { Name = "One", Value = 1 }] + }, + + new Item { Name = "Two", Value = 2 }, + new Item { Name = "Three", Value = 3 } + ] + }; + + var xml = new XmlSerializer(); + var doc = xml.Serialize(items); + var expected = GetNamedItemsXDoc(CultureInfo.InvariantCulture); + + Assert.Equal(expected.ToString(), doc); + } + + [Fact] + public void Can_serialize_a_list_which_is_the_content_of_root_element() { + var contacts = new Contacts { + People = [ + new() { + Name = "Foo", + Age = 50, + Price = 19.95m, + StartDate = new(2009, 12, 18, 10, 2, 23), + Items = [ + new() { Name = "One", Value = 1 }, + new() { Name = "Two", Value = 2 }, + new() { Name = "Three", Value = 3 } + ] + }, + + new() { + Name = "Bar", + Age = 23, + Price = 23.23m, + StartDate = new(2009, 12, 23, 10, 23, 23), + Items = [ + new() { Name = "One", Value = 1 }, + new() { Name = "Two", Value = 2 }, + new() { Name = "Three", Value = 3 } + ] + } + ] + }; + + var xml = new XmlSerializer(); + var doc = xml.Serialize(contacts); + var expected = GetPeopleXDoc(CultureInfo.InvariantCulture); + + Assert.Equal(expected.ToString(), doc); + } + + [Fact] + public void Can_serialize_a_list_which_is_the_root_element() { + var pocoList = new PersonList { + new() { + Name = "Foo", + Age = 50, + Price = 19.95m, + StartDate = new(2009, 12, 18, 10, 2, 23), + Items = [ + new() { Name = "One", Value = 1 }, + new() { Name = "Two", Value = 2 }, + new() { Name = "Three", Value = 3 } + ] + }, + new() { + Name = "Bar", + Age = 23, + Price = 23.23m, + StartDate = new(2009, 12, 23, 10, 23, 23), + Items = [ + new() { Name = "One", Value = 1 }, + new() { Name = "Two", Value = 2 }, + new() { Name = "Three", Value = 3 } + ] + } + }; + var xml = new XmlSerializer(); + var doc = xml.Serialize(pocoList); + var expected = GetPeopleXDoc(CultureInfo.InvariantCulture); + + Assert.Equal(expected.ToString(), doc); + } + + [Fact] + public void Can_Serialize_An_Object_To_Node_With_Attribute_And_Text_Content() { + var note = new Note { + Id = 1, + Title = Note.ConstTitle, + Message = Note.ConstMessage + }; + + var xml = new XmlSerializer(); + var doc = xml.Serialize(note); + + var expected = GetNoteXDoc(); + var expectedStr = expected.ToString(); + + Assert.Equal(expectedStr, doc); + } + + [Fact] + public void Can_serialize_Enum() { + var enumClass = new ClassWithEnum { Color = Color.Red }; + var xml = new XmlSerializer(); + var doc = xml.Serialize(enumClass); + var expected = new XDocument(); + var root = new XElement("ClassWithEnum"); + + root.Add(new XElement("Color", "Red")); + expected.Add(root); + + Assert.Equal(expected.ToString(), doc); + } + + [Fact] + public void Can_serialize_simple_POCO() { + var poco = new Person { + Name = "Foo", + Age = 50, + Price = 19.95m, + StartDate = new(2009, 12, 18, 10, 2, 23), + Items = [ + new() { Name = "One", Value = 1 }, + new() { Name = "Two", Value = 2 }, + new() { Name = "Three", Value = 3 } + ] + }; + var xml = new XmlSerializer(); + var doc = xml.Serialize(poco); + var expected = GetSimplePocoXDoc(); + + Assert.Equal(expected.ToString(), doc); + } + + [Fact] + public void Can_serialize_simple_POCO_With_Attribute_Options_Defined() { + var poco = new WackyPerson { + Name = "Foo", + Age = 50, + Price = 19.95m, + StartDate = new(2009, 12, 18, 10, 2, 23) + }; + var xml = new XmlSerializer(); + var doc = xml.Serialize(poco); + var expected = GetSimplePocoXDocWackyNames(); + + Assert.Equal(expected.ToString(), doc); + } + + [Fact] + public void Can_serialize_simple_POCO_With_Attribute_Options_Defined_And_Property_Containing_IList_Elements() { + var poco = new WackyPerson { + Name = "Foo", + Age = 50, + Price = 19.95m, + StartDate = new(2009, 12, 18, 10, 2, 23), + ContactData = new() { + EmailAddresses = [ + new() { + Address = "test@test.com", + Location = "Work" + } + ] + } + }; + var xml = new XmlSerializer(); + var doc = xml.Serialize(poco); + var expected = GetSimplePocoXDocWackyNamesWithIListProperty(); + + Assert.Equal(expected.ToString(), doc); + } + + [Fact] + public void Can_serialize_simple_POCO_With_DateFormat_Specified() { + var poco = new Person { + Name = "Foo", + Age = 50, + Price = 19.95m, + StartDate = new(2009, 12, 18, 10, 2, 23) + }; + var xml = new XmlSerializer { DateFormat = DateFormat.ISO_8601 }; + var doc = xml.Serialize(poco); + var expected = GetSimplePocoXDocWithIsoDate(); + + Assert.Equal(expected.ToString(), doc); + } + + [Fact] + public void Can_serialize_simple_POCO_With_Different_Root_Element() { + var poco = new Person { + Name = "Foo", + Age = 50, + Price = 19.95m, + StartDate = new(2009, 12, 18, 10, 2, 23) + }; + var xml = new XmlSerializer { RootElement = "Result" }; + var doc = xml.Serialize(poco); + var expected = GetSimplePocoXDocWithRoot(); + + Assert.Equal(expected.ToString(), doc); + } + + [Fact] + public void Can_serialize_simple_POCO_With_XmlFormat_Specified() { + var poco = new Person { + Name = "Foo", + Age = 50, + Price = 19.95m, + StartDate = new(2009, 12, 18, 10, 2, 23), + IsCool = false + }; + var xml = new XmlSerializer { DateFormat = DateFormat.ISO_8601 }; + var doc = xml.Serialize(poco); + var expected = GetSimplePocoXDocWithXmlProperty(); + + Assert.Equal(expected.ToString(), doc); + } + + [Fact] + public void Cannot_Serialize_An_Object_With_Two_Properties_With_Text_Content_Attributes() { + var note = new WrongNote { + Id = 1, + Text = "What a note." + }; + + var xml = new XmlSerializer(); + + Assert.Throws(() => xml.Serialize(note)); + } + + [Fact] + public void Serializes_Properties_In_Specified_Order() { + var ordered = new OrderedProperties { + Name = "Name", + Age = 99, + StartDate = new(2010, 1, 1) + }; + var xml = new XmlSerializer(); + var doc = xml.Serialize(ordered); + var expected = GetSortedPropsXDoc(); + + Assert.Equal(expected.ToString(), doc); + } + + interface INamed { + string Name { get; set; } + } + + class Person : INamed { + public string Name { get; set; } + + public int Age { get; set; } + + public decimal Price { get; set; } + + public DateTime StartDate { get; set; } + + public List Items { get; set; } + + public bool? IsCool { get; set; } + } + + class Item : INamed { + public string Name { get; set; } + + public int Value { get; set; } + } + + enum Color { Red, Blue, Green } + + class ClassWithEnum { + public Color Color { get; set; } + } + + [SerializeAs(Name = "Person")] + class WackyPerson { + [SerializeAs(Name = "WackyName", Attribute = true)] + public string Name { get; set; } + + public int Age { get; set; } + + [SerializeAs(Attribute = true)] + public decimal Price { get; set; } + + [SerializeAs(Name = "start_date", Attribute = true)] + public DateTime StartDate { get; set; } + + [SerializeAs(Name = "contact-data")] + public ContactData ContactData { get; set; } + } + + class NamedItems { + [SerializeAs(Content = true)] + public List Items { get; set; } + } + + [SerializeAs(Name = "People")] + class Contacts { + [SerializeAs(Content = true)] + public List People { get; set; } + } + + [SerializeAs(Name = "People")] + class PersonList : List; + + class ContactData { + [SerializeAs(Name = "email-addresses")] + public List EmailAddresses { get; set; } = []; + } + + [SerializeAs(Name = "email-address")] + class EmailAddress { + [SerializeAs(Name = "address")] + public string Address { get; set; } + + [SerializeAs(Name = "location")] + public string Location { get; set; } + } + + static XDocument GetNoteXDoc() { + var doc = new XDocument(); + var root = new XElement("Note"); + + root.SetAttributeValue("Id", 1); + root.Value = Note.ConstMessage; + root.Add(new XElement("Title", Note.ConstTitle)); + + doc.Add(root); + + return doc; + } + + static XDocument GetSimplePocoXDoc() { + var doc = new XDocument(); + var root = new XElement("Person"); + + root.Add( + new XElement("Name", "Foo"), + new XElement("Age", 50), + new XElement("Price", 19.95m), + new XElement( + "StartDate", + new DateTime(2009, 12, 18, 10, 2, 23).ToString(CultureInfo.InvariantCulture) + ) + ); + + var items = new XElement("Items"); + + items.Add(new XElement("Item", new XElement("Name", "One"), new XElement("Value", 1))); + items.Add(new XElement("Item", new XElement("Name", "Two"), new XElement("Value", 2))); + items.Add(new XElement("Item", new XElement("Name", "Three"), new XElement("Value", 3))); + + root.Add(items); + doc.Add(root); + + return doc; + } + + static XDocument GetSimplePocoXDocWithIsoDate() { + var doc = new XDocument(); + var root = new XElement("Person"); + + root.Add( + new XElement("Name", "Foo"), + new XElement("Age", 50), + new XElement("Price", 19.95m), + new XElement("StartDate", new DateTime(2009, 12, 18, 10, 2, 23).ToString("s")) + ); + + doc.Add(root); + + return doc; + } + + static XDocument GetSimplePocoXDocWithXmlProperty() { + var doc = new XDocument(); + var root = new XElement("Person"); + + root.Add( + new XElement("Name", "Foo"), + new XElement("Age", 50), + new XElement("Price", 19.95m), + new XElement("StartDate", new DateTime(2009, 12, 18, 10, 2, 23).ToString("s")), + new XElement("IsCool", false) + ); + + doc.Add(root); + + return doc; + } + + static XDocument GetSimplePocoXDocWithRoot() { + var doc = new XDocument(); + var root = new XElement("Result"); + var start = new XElement("Person"); + + start.Add( + new XElement("Name", "Foo"), + new XElement("Age", 50), + new XElement("Price", 19.95m), + new XElement( + "StartDate", + new DateTime(2009, 12, 18, 10, 2, 23).ToString(CultureInfo.InvariantCulture) + ) + ); + + root.Add(start); + doc.Add(root); + + return doc; + } + + static XDocument GetSimplePocoXDocWackyNames() { + var doc = new XDocument(); + var root = new XElement("Person"); + + root.Add( + new XAttribute("WackyName", "Foo"), + new XElement("Age", 50), + new XAttribute("Price", 19.95m), + new XAttribute( + "start_date", + new DateTime(2009, 12, 18, 10, 2, 23).ToString(CultureInfo.InvariantCulture) + ) + ); + + doc.Add(root); + + return doc; + } + + static XDocument GetSimplePocoXDocWackyNamesWithIListProperty() { + var doc = new XDocument(); + var root = new XElement("Person"); + + root.Add( + new XAttribute("WackyName", "Foo"), + new XElement("Age", 50), + new XAttribute("Price", 19.95m), + new XAttribute( + "start_date", + new DateTime(2009, 12, 18, 10, 2, 23).ToString(CultureInfo.InvariantCulture) + ), + new XElement( + "contact-data", + new XElement( + "email-addresses", + new XElement( + "email-address", + new XElement("address", "test@test.com"), + new XElement("location", "Work") + ) + ) + ) + ); + + doc.Add(root); + + return doc; + } + + static XDocument GetSortedPropsXDoc() { + var doc = new XDocument(); + var root = new XElement("OrderedProperties"); + + root.Add(new XElement("StartDate", new DateTime(2010, 1, 1).ToString(CultureInfo.InvariantCulture))); + root.Add(new XElement("Name", "Name")); + root.Add(new XElement("Age", 99)); + + doc.Add(root); + + return doc; + } + + static XDocument GetNamedItemsXDoc(IFormatProvider culture) { + var doc = new XDocument(); + var root = new XElement("NamedItems"); + var element = new XElement("Person"); + var items = new XElement("Items"); + + items.Add(new XElement("Item", new XElement("Name", "One"), new XElement("Value", 1))); + + element.Add( + new XElement("Name", "Foo"), + new XElement("Age", 50), + new XElement("Price", 19.95m.ToString(culture)), + new XElement("StartDate", new DateTime(2009, 12, 18, 10, 2, 23).ToString(culture)) + ); + + element.Add(items); + root.Add(element); + root.Add(new XElement("Item", new XElement("Name", "Two"), new XElement("Value", 2))); + root.Add(new XElement("Item", new XElement("Name", "Three"), new XElement("Value", 3))); + + doc.Add(root); + + return doc; + } + + static XDocument GetPeopleXDoc(IFormatProvider culture) { + var doc = new XDocument(); + var root = new XElement("People"); + var element = new XElement("Person"); + var items = new XElement("Items"); + + items.Add(new XElement("Item", new XElement("Name", "One"), new XElement("Value", 1))); + items.Add(new XElement("Item", new XElement("Name", "Two"), new XElement("Value", 2))); + items.Add(new XElement("Item", new XElement("Name", "Three"), new XElement("Value", 3))); + + element.Add( + new XElement("Name", "Foo"), + new XElement("Age", 50), + new XElement("Price", 19.95m.ToString(culture)), + new XElement("StartDate", new DateTime(2009, 12, 18, 10, 2, 23).ToString(culture)) + ); + + element.Add(items); + root.Add(element); + element = new("Person"); + + element.Add( + new XElement("Name", "Bar"), + new XElement("Age", 23), + new XElement("Price", 23.23m.ToString(culture)), + new XElement("StartDate", new DateTime(2009, 12, 23, 10, 23, 23).ToString(culture)) + ); + + element.Add(items); + + root.Add(element); + doc.Add(root); + + return doc; + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Shared/Extensions/StreamExtensions.cs b/test/RestSharp.Tests.Shared/Extensions/StreamExtensions.cs new file mode 100644 index 000000000..b4fd142ce --- /dev/null +++ b/test/RestSharp.Tests.Shared/Extensions/StreamExtensions.cs @@ -0,0 +1,23 @@ +using System.Text; + +namespace RestSharp.Tests.Shared.Extensions; + +public static class StreamExtensions { + public static void WriteStringUtf8(this Stream target, string value) { + var encoded = Encoding.UTF8.GetBytes(value); + + target.Write(encoded, 0, encoded.Length); + } + + public static string StreamToString(this Stream stream) { + using var streamReader = new StreamReader(stream); + + return streamReader.ReadToEnd(); + } + + public static async Task StreamToStringAsync(this Stream stream) { + using var streamReader = new StreamReader(stream); + + return await streamReader.ReadToEndAsync().ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Shared/Extensions/UriExtensions.cs b/test/RestSharp.Tests.Shared/Extensions/UriExtensions.cs new file mode 100644 index 000000000..c1c512ad0 --- /dev/null +++ b/test/RestSharp.Tests.Shared/Extensions/UriExtensions.cs @@ -0,0 +1,8 @@ +namespace RestSharp.Tests.Shared.Extensions; + +public static class UriExtensions { + public static IDictionary ParseQuery(this Uri uri) { + var query = uri.Query[1..].Split('&'); + return query.Select(x => x.Split('=')).ToDictionary(x => x[0], x => x[1]); + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Shared/Extensions/WireMockExtensions.cs b/test/RestSharp.Tests.Shared/Extensions/WireMockExtensions.cs new file mode 100644 index 000000000..10751dcc4 --- /dev/null +++ b/test/RestSharp.Tests.Shared/Extensions/WireMockExtensions.cs @@ -0,0 +1,22 @@ +using RestSharp.Tests.Shared.Fixtures; +using WireMock.RequestBuilders; +using WireMock.ResponseBuilders; +using WireMock.Server; + +namespace RestSharp.Tests.Shared.Extensions; + +public static class WireMockExtensions { + public static RequestBodyCapturer ConfigureBodyCapturer(this WireMockServer server, Method method, bool usePath = true) { + var capturer = new RequestBodyCapturer(); + + var requestBuilder = Request + .Create() + .WithPath(usePath ? RequestBodyCapturer.Resource : "/") + .WithUrl(capturer.CaptureUrl) + .WithBody(capturer.CaptureBody) + .WithHeader(capturer.CaptureHeaders) + .UsingMethod(method.ToString().ToUpper()); + server.Given(requestBuilder).RespondWith(Response.Create().WithStatusCode(200)); + return capturer; + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Shared/Fixtures/Handlers.cs b/test/RestSharp.Tests.Shared/Fixtures/Handlers.cs new file mode 100644 index 000000000..8cd404dd4 --- /dev/null +++ b/test/RestSharp.Tests.Shared/Fixtures/Handlers.cs @@ -0,0 +1,38 @@ +using System.Net; +using System.Reflection; + +namespace RestSharp.Tests.Shared.Fixtures; + +public static class Handlers { + /// + /// T should be a class that implements methods whose names match the urls being called, and take one parameter, an + /// HttpListenerContext. + /// e.g. + /// urls exercised: "http://localhost:8888/error" and "http://localhost:8888/get_list" + /// class MyHandler + /// { + /// void error(HttpListenerContext ctx) + /// { + /// // do something interesting here + /// } + /// void get_list(HttpListenerContext ctx) + /// { + /// // do something interesting here + /// } + /// } + /// + public static Action Generic() where T : new() + => ctx => { + var methodName = ctx.Request.Url!.Segments.Last(); + + var method = typeof(T).GetMethod( + methodName, + BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static + ); + + if (method!.IsStatic) + method.Invoke(null, [ctx]); + else + method.Invoke(new T(), [ctx]); + }; +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Shared/Fixtures/RequestBodyCapturer.cs b/test/RestSharp.Tests.Shared/Fixtures/RequestBodyCapturer.cs new file mode 100644 index 000000000..cc9ead4fb --- /dev/null +++ b/test/RestSharp.Tests.Shared/Fixtures/RequestBodyCapturer.cs @@ -0,0 +1,29 @@ +namespace RestSharp.Tests.Shared.Fixtures; + +public class RequestBodyCapturer { + public const string Resource = "/capture"; + + public string ContentType { get; private set; } + public bool HasBody { get; private set; } + public string Body { get; private set; } + public Uri Url { get; private set; } + + public bool CaptureBody(string content) { + Body = content; + HasBody = !string.IsNullOrWhiteSpace(content); + return true; + } + + public bool CaptureHeaders(IDictionary headers) { + if (headers.TryGetValue("Content-Type", out var contentType)) { + ContentType = contentType[0]; + } + + return true; + } + + public bool CaptureUrl(string url) { + Url = new(url); + return true; + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Shared/Fixtures/SimpleServer.cs b/test/RestSharp.Tests.Shared/Fixtures/SimpleServer.cs new file mode 100644 index 000000000..5f6abc2f9 --- /dev/null +++ b/test/RestSharp.Tests.Shared/Fixtures/SimpleServer.cs @@ -0,0 +1,38 @@ +using System.Net; + +namespace RestSharp.Tests.Shared.Fixtures; + +public sealed class SimpleServer : IDisposable { + static readonly Random Random = new(DateTimeOffset.Now.Millisecond); + + readonly WebServer _server; + readonly CancellationTokenSource _cts = new(); + + public string Url { get; } + + SimpleServer( + int port, + Action handler = null, + AuthenticationSchemes authenticationSchemes = AuthenticationSchemes.Anonymous + ) { + Url = $"http://localhost:{port}/"; + _server = new(Url, handler, authenticationSchemes); + Task.Run(() => _server.Run(_cts.Token)); + } + + public void Dispose() { + _cts.Cancel(); + _server.Stop(); + _cts.Dispose(); + } + + public static SimpleServer Create( + Action handler = null, + AuthenticationSchemes authenticationSchemes = AuthenticationSchemes.Anonymous + ) { + var port = Random.Next(1000, 9999); + return new(port, handler, authenticationSchemes); + } + + public void SetHandler(Action handler) => _server.ChangeHandler(handler); +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Shared/Fixtures/WebServer.cs b/test/RestSharp.Tests.Shared/Fixtures/WebServer.cs new file mode 100644 index 000000000..364ea6751 --- /dev/null +++ b/test/RestSharp.Tests.Shared/Fixtures/WebServer.cs @@ -0,0 +1,70 @@ +using System.Net; + +namespace RestSharp.Tests.Shared.Fixtures; + +public class WebServer { + readonly HttpListener _listener = new(); + Action _responderMethod; + + public WebServer(string prefix, Action method, AuthenticationSchemes authenticationSchemes) { + if (string.IsNullOrEmpty(prefix)) + throw new ArgumentException("URI prefix is required"); + + _listener.Prefixes.Add(prefix); + _listener.AuthenticationSchemes = authenticationSchemes; + + _responderMethod = method; + } + + public async Task Run(CancellationToken token) { + var taskFactory = new TaskFactory(token); + _listener.Start(); + + try { + while (!token.IsCancellationRequested && _listener.IsListening) { + try { + var ctx = await GetContextAsync(); + // ReSharper disable once ConditionIsAlwaysTrueOrFalse + if (ctx == null) continue; + + _responderMethod?.Invoke(ctx); + ctx.Response.OutputStream.Close(); + } + catch (Exception e) { + Console.WriteLine(e.ToString()); + } + } + } + catch (Exception e) { + Console.WriteLine(e.ToString()); + } + + return; + + Task GetContextAsync() + => taskFactory.FromAsync( + (callback, state) => ((HttpListener)state!).BeginGetContext(callback, state), + iar => { + try { + return ((HttpListener)iar.AsyncState!).EndGetContext(iar); + } + catch (ObjectDisposedException) { + // it's ok + return null; + } + catch (HttpListenerException) { + // it's ok + return null; + } + }, + _listener + ); + } + + public void Stop() { + _listener.Stop(); + _listener.Close(); + } + + public void ChangeHandler(Action handler) => _responderMethod = handler; +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Shared/RequestTestsBase.cs b/test/RestSharp.Tests.Shared/RequestTestsBase.cs new file mode 100644 index 000000000..bc4d416c0 --- /dev/null +++ b/test/RestSharp.Tests.Shared/RequestTestsBase.cs @@ -0,0 +1,117 @@ +using System.Net; +using RestSharp.Serializers; +using RestSharp.Tests.Shared.Server; +using WireMock.ResponseBuilders; + +namespace RestSharp.Tests.Shared; + +public abstract class RequestTestsBase(bool disposeClient) { + protected abstract IRestClient GetClient(); + + IRestClient GetTestClient() => new TestClient(GetClient(), disposeClient); + + [Fact] + public async Task Can_Handle_Exception_Thrown_By_Interceptor_BeforeDeserialization() { + const string exceptionMessage = "Thrown from OnBeforeDeserialization"; + + var request = new RestRequest("success") { + Interceptors = [new ThrowingInterceptor(exceptionMessage)] + }; + + using var client = GetTestClient(); + var response = await client.ExecuteAsync(request); + + Assert.Equal(exceptionMessage, response.ErrorMessage); + Assert.Equal(ResponseStatus.Error, response.ResponseStatus); + } + + [Fact, Obsolete("Obsolete")] + public async Task Can_Handle_Exception_Thrown_By_OnBeforeDeserialization_Handler() { + const string exceptionMessage = "Thrown from OnBeforeDeserialization"; + + var request = new RestRequest("success"); + + request.OnBeforeDeserialization += _ => throw new Exception(exceptionMessage); + + using var client = GetTestClient(); + var response = await client.ExecuteAsync(request); + + Assert.Equal(exceptionMessage, response.ErrorMessage); + Assert.Equal(ResponseStatus.Error, response.ResponseStatus); + } + + [Fact] + public async Task Can_Perform_ExecuteGetAsync_With_Response_Type() { + using var client = GetTestClient(); + var request = new RestRequest("success"); + var response = await client.ExecuteAsync(request); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + response.Data!.Message.Should().Be("Works!"); + } + + [Fact] + public async Task Can_Perform_GET_Async() { + const string val = "Basic async test"; + + var request = new RestRequest($"echo?msg={val}"); + + using var client = GetTestClient(); + var response = await client.ExecuteAsync(request); + response.Content.Should().Be(val); + } + +#if NET + [Fact] + public async Task Can_Timeout_GET_Async() { + var request = new RestRequest("timeout").AddBody("Body_Content"); + + // Half the value of ResponseHandler.Timeout + request.Timeout = TimeSpan.FromMilliseconds(200); + + using var client = GetTestClient(); + var response = await client.ExecuteAsync(request); + + response.ResponseStatus.Should().Be(ResponseStatus.TimedOut, response.ErrorMessage); + } +#endif + + [Fact] + public async Task Can_Perform_Delete_With_Response_Type() { + using var client = GetTestClient(); + var request = new RestRequest("delete"); + var response = await client.ExecuteAsync(request, Method.Delete); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + response.Data!.Message.Should().Be("Works!"); + } + + [Fact] + public async Task Can_Delete_With_Response_Type_using_extension() { + using var client = GetTestClient(); + var request = new RestRequest("delete"); + var response = await client.DeleteAsync(request); + + response!.Message.Should().Be("Works!"); + } + + class ThrowingInterceptor(string errorMessage) : Interceptors.Interceptor { + public override ValueTask BeforeDeserialization(RestResponse response, CancellationToken cancellationToken) => throw new(errorMessage); + } + + class TestClient(IRestClient innerClient, bool disposeClient) : IRestClient { + public void Dispose() { + if (disposeClient) innerClient.Dispose(); + } + + public ReadOnlyRestClientOptions Options => innerClient.Options; + public RestSerializers Serializers => innerClient.Serializers; + public DefaultParameters DefaultParameters => innerClient.DefaultParameters; + + public Task ExecuteAsync(RestRequest request, CancellationToken cancellationToken = default) + => innerClient.ExecuteAsync(request, cancellationToken); + + public Task DownloadStreamAsync(RestRequest request, CancellationToken cancellationToken = default) + => innerClient.DownloadStreamAsync(request, cancellationToken); + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Shared/RestSharp.Tests.Shared.csproj b/test/RestSharp.Tests.Shared/RestSharp.Tests.Shared.csproj new file mode 100644 index 000000000..06f32e8e6 --- /dev/null +++ b/test/RestSharp.Tests.Shared/RestSharp.Tests.Shared.csproj @@ -0,0 +1,11 @@ + + + false + + + + + + + + diff --git a/test/RestSharp.Tests.Shared/Server/Models.cs b/test/RestSharp.Tests.Shared/Server/Models.cs new file mode 100644 index 000000000..9ae54a400 --- /dev/null +++ b/test/RestSharp.Tests.Shared/Server/Models.cs @@ -0,0 +1,11 @@ +namespace RestSharp.Tests.Shared.Server; + +public record TestServerResponse(string Name, string Value); + +public record UploadResponse(string FileName, long Length, bool Equal); + +public record SuccessResponse(string Message); + +public class TestResponse { + public string Message { get; set; } = null!; +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Shared/Server/WireMockTestServer.cs b/test/RestSharp.Tests.Shared/Server/WireMockTestServer.cs new file mode 100644 index 000000000..527e3d5a0 --- /dev/null +++ b/test/RestSharp.Tests.Shared/Server/WireMockTestServer.cs @@ -0,0 +1,106 @@ +using System.Net; +using System.Text.Json; +using WireMock; +using WireMock.RequestBuilders; +using WireMock.ResponseBuilders; +using WireMock.Server; +using WireMock.Types; +using WireMock.Util; + +namespace RestSharp.Tests.Shared.Server; + +// ReSharper disable once ClassNeverInstantiated.Global +public class WireMockTestServer : WireMockServer { + public WireMockTestServer() : base(new() { Port = 0, UseHttp2 = false, UseSSL = false }) { + Given(Request.Create().WithPath("/echo")) + .RespondWith(Response.Create().WithCallback(EchoQuery)); + + Given(Request.Create().WithPath("/success").UsingGet()) + .RespondWith(Response.Create().WithBodyAsJson(new SuccessResponse("Works!"))); + + Given(Request.Create().WithPath("/delete").UsingDelete()) + .RespondWith(Response.Create().WithBodyAsJson(new SuccessResponse("Works!"))); + + Given(Request.Create().WithPath("/content")) + .RespondWith(Response.Create().WithCallback(EchoJsonBody)); + + Given(Request.Create().WithPath("/post/json").UsingPost()) + .RespondWith(Response.Create().WithCallback(WrapBody)); + + Given(Request.Create().WithPath("/post/data").UsingPost()) + .RespondWith(Response.Create().WithCallback(HandleForm)); + + Given(Request.Create().WithPath("/post/form").UsingPost()) + .RespondWith(Response.Create().WithCallback(WrapForm)); + + Given(Request.Create().WithPath("/timeout")) + .RespondWith(Response.Create().WithDelay(1000)); + + Given(Request.Create().WithPath("/redirect")) + .RespondWith(Response.Create().WithStatusCode(HttpStatusCode.Redirect).WithHeader("Location", "/success")); + + Given(Request.Create().WithPath("/status").UsingGet()) + .RespondWith(Response.Create().WithCallback(StatusCode)); + + Given(Request.Create().WithPath("/headers")) + .RespondWith(Response.Create().WithCallback(EchoHeaders)); + } + + static ResponseMessage WrapForm(IRequestMessage request) { + var response = request.BodyData!.BodyAsFormUrlEncoded!["big_string"].Length; + return CreateJson(new SuccessResponse($"Works! Length: {response}")); + } + + static readonly JsonSerializerOptions JsonOptions = new() { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = true + }; + + static ResponseMessage HandleForm(IRequestMessage request) { + var result = request.BodyData!.BodyAsFormUrlEncoded!.Select(x => new TestServerResponse(x.Key, x.Value)); + return CreateJson(result); + } + + static ResponseMessage EchoQuery(IRequestMessage request) { + var query = request.Query!["msg"]; + var msg = query[0]; + + return new ResponseMessage { + BodyData = new BodyData { + DetectedBodyType = BodyType.String, + BodyAsString = msg + } + }; + } + + static ResponseMessage EchoHeaders(IRequestMessage request) { + var headers = request.Headers!.Select(x => new TestServerResponse(x.Key, x.Value.First())); + return CreateJson(headers); + } + + static ResponseMessage EchoJsonBody(IRequestMessage request) => CreateJson(request.BodyAsJson!); + + static ResponseMessage WrapBody(IRequestMessage request) { + var data = JsonSerializer.Deserialize(request.Body!, JsonOptions); + return CreateJson(new TestResponse { Message = data?.Data ?? "" }); + } + + static ResponseMessage StatusCode(IRequestMessage request) { + var query = request.Query!["code"]; + var statusCode = int.Parse(query[0]); + + return new ResponseMessage { + StatusCode = statusCode + }; + } + + public static ResponseMessage CreateJson(object response) + => new() { + BodyData = new BodyData { + BodyAsJson = response, + DetectedBodyType = BodyType.Json + } + }; +} + +public record TestRequest(string Data, int Number); \ No newline at end of file diff --git a/test/RestSharp.Tests/Auth/AuthenticatorTests.cs b/test/RestSharp.Tests/Auth/AuthenticatorTests.cs new file mode 100644 index 000000000..4129a76a6 --- /dev/null +++ b/test/RestSharp.Tests/Auth/AuthenticatorTests.cs @@ -0,0 +1,42 @@ +using System.Net; +using RestSharp.Authenticators; +using RestSharp.Tests.Fixtures; +using RichardSzalay.MockHttp; + +namespace RestSharp.Tests.Auth; + +public class AuthenticatorTests { + [Fact] + public async Task Should_add_authorization_header() { + const string auth = "LetMeIn"; + + using var client = MockHttpClient.Create( + Method.Get, + request => request.WithHeaders(KnownHeaders.Authorization, auth).Respond(HttpStatusCode.OK), + opt => opt.Authenticator = new TestAuthenticator(ParameterType.HttpHeader, KnownHeaders.Authorization, auth) + ); + var response = await client.ExecuteGetAsync(new RestRequest()); + response.StatusCode.Should().Be(HttpStatusCode.OK); + } + + [Fact] + public async Task Should_add_authorization_form_parameter() { + const string auth = "LetMeIn"; + const string formField = "token"; + + using var client = MockHttpClient.Create( + Method.Post, + request => request.WithFormData(formField, auth).Respond(HttpStatusCode.OK), + opt => opt.Authenticator = new TestAuthenticator(ParameterType.GetOrPost, formField, auth) + ); + var response = await client.ExecutePostAsync(new RestRequest()); + response.StatusCode.Should().Be(HttpStatusCode.OK); + } + + class TestAuthenticator(ParameterType type, string name, string value) : IAuthenticator { + public ValueTask Authenticate(IRestClient client, RestRequest request) { + request.AddParameter(name, value, type); + return default; + } + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests/Auth/HttpBasicAuthTests.cs b/test/RestSharp.Tests/Auth/HttpBasicAuthTests.cs new file mode 100644 index 000000000..a8f3ea3ef --- /dev/null +++ b/test/RestSharp.Tests/Auth/HttpBasicAuthTests.cs @@ -0,0 +1,27 @@ +using System.Text; +using RestSharp.Authenticators; + +namespace RestSharp.Tests.Auth; + +public class HttpBasicAuthTests { + const string Username = "username"; + const string Password = "password"; + + readonly HttpBasicAuthenticator _auth = new(Username, Password); + + [Fact] + public async Task Authenticate_ShouldAddAuthorizationParameter_IfPreviouslyUnassigned() { + // Arrange + using var client = new RestClient(); + + var request = new RestRequest(); + request.AddQueryParameter("NotMatching", "", default); + var expectedToken = $"Basic {Convert.ToBase64String(Encoding.UTF8.GetBytes($"{Username}:{Password}"))}"; + + // Act + await _auth.Authenticate(client, request); + + // Assert + request.Parameters.Single(x => x.Name == KnownHeaders.Authorization).Value.Should().Be(expectedToken); + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests/Auth/JwtAuthTests.cs b/test/RestSharp.Tests/Auth/JwtAuthTests.cs new file mode 100644 index 000000000..484d841ed --- /dev/null +++ b/test/RestSharp.Tests/Auth/JwtAuthTests.cs @@ -0,0 +1,124 @@ +using System.Globalization; +using RestSharp.Authenticators; + +namespace RestSharp.Tests.Auth; + +public class JwtAuthTests { + readonly string _testJwt; + readonly string _expectedAuthHeaderContent; + + public JwtAuthTests() { + Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture; + Thread.CurrentThread.CurrentUICulture = CultureInfo.InstalledUICulture; + + _testJwt = "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9" + + "." + + "eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQo" + + "gImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ" + + "." + + "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"; + + _expectedAuthHeaderContent = $"Bearer {_testJwt}"; + } + + [Fact] + public async Task Can_Set_ValidFormat_Auth_Header() { + using var client = new RestClient(new RestClientOptions { Authenticator = new JwtAuthenticator(_testJwt) }); + + var request = new RestRequest(); + //In real case client.Execute(request) will invoke Authenticate method + await client.Options.Authenticator!.Authenticate(client, request); + + var authParam = request.Parameters.Single(p => p.Name!.Equals(KnownHeaders.Authorization, StringComparison.OrdinalIgnoreCase)); + + Assert.Equal(ParameterType.HttpHeader, authParam.Type); + Assert.Equal(_expectedAuthHeaderContent, authParam.Value); + } + + [Fact] + public async Task Can_Set_ValidFormat_Auth_Header_With_Bearer_Prefix() { + using var client = new RestClient(new RestClientOptions { Authenticator = new JwtAuthenticator($"Bearer {_testJwt}") }); + + var request = new RestRequest(); + //In real case client.Execute(request) will invoke Authenticate method + await client.Options.Authenticator!.Authenticate(client, request); + + var authParam = request.Parameters.Single(p => p.Name!.Equals(KnownHeaders.Authorization, StringComparison.OrdinalIgnoreCase)); + + Assert.Equal(ParameterType.HttpHeader, authParam.Type); + Assert.Equal(_expectedAuthHeaderContent, authParam.Value); + } + + [Fact] + public async Task Check_Only_Header_Authorization() { + using var client = new RestClient(new RestClientOptions { Authenticator = new JwtAuthenticator(_testJwt) }); + + var request = new RestRequest(); + // Paranoid server needs "two-factor authentication": JWT header and query param key for example + request.AddParameter(KnownHeaders.Authorization, "manualAuth", ParameterType.QueryString); + + // In real case client.Execute(request) will invoke Authenticate method + await client.Options.Authenticator!.Authenticate(client, request); + + var paramList = request.Parameters.Where(p => p.Name!.Equals(KnownHeaders.Authorization)).ToList(); + + Assert.Equal(2, paramList.Count); + + var queryAuthParam = paramList.Single(p => p.Type.Equals(ParameterType.QueryString)); + var headerAuthParam = paramList.Single(p => p.Type.Equals(ParameterType.HttpHeader)); + + Assert.Equal("manualAuth", queryAuthParam.Value); + Assert.Equal(_expectedAuthHeaderContent, headerAuthParam.Value); + } + + [Fact] + public async Task Set_Auth_Header_Only_Once() { + using var client = new RestClient(new RestClientOptions { Authenticator = new JwtAuthenticator(_testJwt) }); + + var request = new RestRequest(); + request.AddHeader(KnownHeaders.Authorization, "second_header_auth_token"); + + //In real case client.Execute(...) will invoke Authenticate method + await client.Options.Authenticator!.Authenticate(client, request); + + var paramList = request.Parameters.Where(p => p.Name.Equals(KnownHeaders.Authorization)).ToList(); + + paramList.Should().HaveCount(1); + + var authParam = paramList[0]; + + Assert.Equal(ParameterType.HttpHeader, authParam.Type); + Assert.Equal(_expectedAuthHeaderContent, authParam.Value); + Assert.NotEqual("Bearer second_header_auth_token", authParam.Value); + } + + [Fact] + public async Task Updates_Auth_Header() { + var request = new RestRequest(); + + var authenticator = new JwtAuthenticator(_expectedAuthHeaderContent); + + using var client = new RestClient(new RestClientOptions { Authenticator = authenticator }); + await client.Options.Authenticator!.Authenticate(client, request); + + authenticator.SetBearerToken("second_header_auth_token"); + await client.Options.Authenticator.Authenticate(client, request); + + var paramList = request.Parameters.Where(p => p.Name.Equals(KnownHeaders.Authorization)).ToList(); + + Assert.Single(paramList); + + var authParam = paramList[0]; + + Assert.Equal(ParameterType.HttpHeader, authParam.Type); + Assert.NotEqual(_expectedAuthHeaderContent, authParam.Value); + Assert.Equal("Bearer second_header_auth_token", authParam.Value); + } + + [Fact] + public void Throw_Argument_Null_Exception() { + var exception = Assert.Throws(() => new JwtAuthenticator(null!)); + + Assert.Equal("accessToken", exception.ParamName); + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests/Auth/OAuth1AuthTests.cs b/test/RestSharp.Tests/Auth/OAuth1AuthTests.cs new file mode 100644 index 000000000..22cf3e1b9 --- /dev/null +++ b/test/RestSharp.Tests/Auth/OAuth1AuthTests.cs @@ -0,0 +1,167 @@ +using RestSharp.Authenticators; +using RestSharp.Authenticators.OAuth; + +namespace RestSharp.Tests.Auth; + +public class OAuth1AuthTests { + readonly OAuth1Authenticator _auth = new() { + CallbackUrl = "CallbackUrl", + ClientPassword = "ClientPassword", + Type = OAuthType.ClientAuthentication, + ClientUsername = "ClientUsername", + ConsumerKey = "ConsumerKey", + ConsumerSecret = "ConsumerSecret", + Realm = "Realm", + SessionHandle = "SessionHandle", + SignatureMethod = OAuthSignatureMethod.PlainText, + SignatureTreatment = OAuthSignatureTreatment.Escaped, + Token = "Token", + TokenSecret = "TokenSecret", + Verifier = "Verifier", + Version = "Version" + }; + + [Fact] + public void Authenticate_ShouldAddAuthorizationAsTextValueToRequest_OnHttpAuthorizationHeaderHandling() { + // Arrange + const string url = "https://no-query.string"; + + using var client = new RestClient(url); + var request = new RestRequest(); + + _auth.ParameterHandling = OAuthParameterHandling.HttpAuthorizationHeader; + + // Act + _auth.Authenticate(client, request); + + // Assert + var authParameter = request.Parameters.Single(x => x.Name == KnownHeaders.Authorization); + var value = (string)authParameter.Value; + + Assert.Contains("OAuth", value); + Assert.Contains("realm=\"Realm\"", value); + Assert.Contains("oauth_timestamp=", value); + Assert.Contains("oauth_signature=\"ConsumerSecret", value); + Assert.Contains("oauth_nonce=", value); + Assert.Contains("oauth_consumer_key=\"ConsumerKey\"", value); + Assert.Contains("oauth_signature_method=\"PLAINTEXT\"", value); + Assert.Contains("oauth_version=\"Version\"", value); + Assert.Contains("x_auth_mode=\"client_auth\"", value); + Assert.Contains("x_auth_username=\"ClientUsername\"", value); + Assert.Contains("x_auth_password=\"ClientPassword\"", value); + } + + [Fact] + public void Authenticate_ShouldAddSignatureToRequestAsSeparateParameters_OnUrlOrPostParametersHandling() { + // Arrange + const string url = "https://no-query.string"; + + using var client = new RestClient(url); + var request = new RestRequest(); + request.AddQueryParameter("queryparameter", "foobartemp"); + + _auth.ParameterHandling = OAuthParameterHandling.UrlOrPostParameters; + + // Act + _auth.Authenticate(client, request); + + // Assert + var parameters = request.Parameters; + ParameterShouldBe("x_auth_username", "ClientUsername"); + ParameterShouldBe("x_auth_password", "ClientPassword"); + ParameterShouldBe("x_auth_mode", "client_auth"); + ParameterShouldBe("oauth_consumer_key", "ConsumerKey"); + ParameterShouldHaveValue("oauth_signature"); + ParameterShouldBe("oauth_signature_method", "PLAINTEXT"); + ParameterShouldBe("oauth_version", "Version"); + ParameterShouldHaveValue("oauth_nonce"); + ParameterShouldHaveValue("oauth_timestamp"); + return; + + void ParameterShould(string name, Func check) { + var parameter = parameters.FirstOrDefault(x => x.Type == ParameterType.GetOrPost && x.Name == name); + parameter.Should().NotBeNull(); + check(parameter).Should().BeTrue(); + } + + void ParameterShouldBe(string name, string value) => ParameterShould(name, x => (string)x.Value == value); + + void ParameterShouldHaveValue(string name) => ParameterShould(name, x => !string.IsNullOrWhiteSpace((string)x.Value)); + } + + [Theory] + [InlineData(OAuthType.AccessToken, "Token", "Token")] + [InlineData(OAuthType.ProtectedResource, "Token", "Token")] + [InlineData(OAuthType.AccessToken, "SVyDD+RsFzSoZChk=", "SVyDD%2BRsFzSoZChk%3D")] + [InlineData(OAuthType.ProtectedResource, "SVyDD+RsFzSoZChk=", "SVyDD%2BRsFzSoZChk%3D")] + public void Authenticate_ShouldEncodeOAuthTokenParameter(OAuthType type, string value, string expected) { + // Arrange + const string url = "https://no-query.string"; + + using var client = new RestClient(url); + var request = new RestRequest(); + _auth.Type = type; + _auth.Token = value; + + // Act + _auth.Authenticate(client, request); + + // Assert + var authParameter = request.Parameters.Single(x => x.Name == KnownHeaders.Authorization); + var authHeader = (string)authParameter.Value; + + Assert.NotNull(authHeader); + Assert.Contains($"oauth_token=\"{expected}\"", authHeader); + } + + /// + /// According to the specifications of OAuth 1.0a, the customer secret is not required. + /// For more information, check the section 4 on https://oauth.net/core/1.0a/. + /// + [Theory] + [InlineData(OAuthType.AccessToken)] + [InlineData(OAuthType.ProtectedResource)] + public void Authenticate_ShouldAllowEmptyConsumerSecret_OnHttpAuthorizationHeaderHandling(OAuthType type) { + // Arrange + const string url = "https://no-query.string"; + + using var client = new RestClient(url); + var request = new RestRequest(); + _auth.Type = type; + _auth.ConsumerSecret = null; + + // Act + _auth.Authenticate(client, request); + + // Assert + var authParameter = request.Parameters.Single(x => x.Name == KnownHeaders.Authorization); + var value = (string)authParameter.Value; + + Assert.NotNull(value); + Assert.NotEmpty(value); + Assert.Contains("OAuth", value!); + Assert.Contains($"oauth_signature=\"{OAuthTools.UrlEncodeStrict("&")}", value); + } + + [Fact] + public async Task Authenticate_ShouldUriEncodeConsumerKey_OnHttpAuthorizationHeaderHandling() { + // Arrange + const string url = "https://no-query.string"; + + var client = new RestClient(url); + var request = new RestRequest(); + _auth.Type = OAuthType.ProtectedResource; + _auth.ConsumerKey = "my@consumer!key"; + _auth.ConsumerSecret = null; + + // Act + await _auth.Authenticate(client, request); + + // Assert + var authParameter = request.Parameters.Single(x => x.Name == KnownHeaders.Authorization); + var value = (string)authParameter.Value; + + value.Should().Contain("OAuth"); + value.Should().Contain("oauth_consumer_key=\"my%40consumer%21key"); + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests/Auth/OAuth1SignatureTests.cs b/test/RestSharp.Tests/Auth/OAuth1SignatureTests.cs new file mode 100644 index 000000000..30ed0ae71 --- /dev/null +++ b/test/RestSharp.Tests/Auth/OAuth1SignatureTests.cs @@ -0,0 +1,123 @@ +using RestSharp.Authenticators; +using RestSharp.Authenticators.OAuth; + +namespace RestSharp.Tests.Auth; + +public class OAuth1SignatureTests { + readonly OAuthWorkflow _workflow = new() { + ParameterHandling = OAuthParameterHandling.UrlOrPostParameters, + Token = "370773112-GmHxMAgYyLbNEtIKZeRNFsMKPR9EyMZeS9weJAEb", + TokenSecret = "LswwdoUaIvS8ltyTt5jkRh4J50vUPVVHtR2YPi5kE", + ConsumerKey = "xvz1evFS4wEEPTGEFPHBog", + ConsumerSecret = "kAcSOqF21Fu85e7zjz7ZN2U4ZRhfV3WpwPAoE3Z7kBw", + SignatureMethod = OAuthSignatureMethod.HmacSha1, + Version = "1.0", + GetNonce = () => "kYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4cg", + GetTimestamp = () => "1318622958" + }; + + readonly RestClient _client = new("https://api.twitter.com/1.1"); + + readonly RestRequest _request = new RestRequest("statuses/update.json", Method.Post) + .AddParameter("status", "Hello Ladies + Gentlemen, a signed OAuth request!") + .AddParameter("include_entities", "true"); + + [Fact] + public void Adds_correct_signature() { + OAuth1Authenticator.AddOAuthData(_client, _request, _workflow, OAuthType.ProtectedResource, null); + + var signature = _request.Parameters.First(x => x.Name == "oauth_signature").Value; + signature.Should().Be("hCtSmYh+iHYCEqBWrE7C7hYmtUk="); + } + + [Fact] + public void Generates_correct_signature_base() { + const string method = "POST"; + + var requestParameters = _request.Parameters.ToWebParameters().ToArray(); + var parameters = new WebPairCollection(); + parameters.AddRange(requestParameters); + var url = _client.BuildUri(_request).ToString(); + _workflow.RequestUrl = url; + var oauthParameters = _workflow.BuildProtectedResourceSignature(method, parameters); + oauthParameters.Parameters.AddRange(requestParameters); + + var signatureBase = OAuthTools.ConcatenateRequestElements(method, url, oauthParameters.Parameters); + + signatureBase.Should() + .Be( + "POST&https%3A%2F%2Fapi.twitter.com%2F1.1%2Fstatuses%2Fupdate.json&include_entities%3Dtrue%26oauth_consumer_key%3Dxvz1evFS4wEEPTGEFPHBog%26oauth_nonce%3DkYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4cg%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1318622958%26oauth_token%3D370773112-GmHxMAgYyLbNEtIKZeRNFsMKPR9EyMZeS9weJAEb%26oauth_version%3D1.0%26status%3DHello%2520Ladies%2520%252B%2520Gentlemen%252C%2520a%2520signed%2520OAuth%2520request%2521" + ); + } + + [Fact] + public void Handles_path_with_exclamation_mark() { + // Test that a path segment with ! is encoded correctly in the signature base + var client = new RestClient("https://api.example.com"); + var request = new RestRequest("path/with!exclamation/resource", Method.Get); + + const string method = "GET"; + var url = client.BuildUri(request).ToString(); + var parameters = new WebPairCollection(); + + _workflow.RequestUrl = url; + var oauthParameters = _workflow.BuildProtectedResourceSignature(method, parameters); + + var signatureBase = OAuthTools.ConcatenateRequestElements(method, url, oauthParameters.Parameters); + + // The URL should be encoded with ! as %21 in the signature base + signatureBase.Should().Contain("path%2Fwith%21exclamation%2Fresource"); + } + + [Theory] + [InlineData("path/with!exclamation", "%21")] + [InlineData("path/with*asterisk", "%2A")] + [InlineData("path/with'apostrophe", "%27")] + [InlineData("path/with(paren", "%28")] + [InlineData("path/with)paren", "%29")] + public void Encodes_RFC3986_special_chars_in_path(string path, string encodedChar) { + // Test that RFC 3986 special characters are properly encoded in path segments + var client = new RestClient("https://api.example.com"); + var request = new RestRequest(path, Method.Get); + + const string method = "GET"; + var url = client.BuildUri(request).ToString(); + var parameters = new WebPairCollection(); + + _workflow.RequestUrl = url; + var oauthParameters = _workflow.BuildProtectedResourceSignature(method, parameters); + + var signatureBase = OAuthTools.ConcatenateRequestElements(method, url, oauthParameters.Parameters); + + // The URL should contain the encoded character in the signature base + signatureBase.Should().Contain(encodedChar); + } + + [Theory] + [InlineData("with!exclamation")] + [InlineData("with*asterisk")] + [InlineData("with'apostrophe")] + [InlineData("with(paren")] + [InlineData("with)paren")] + public void Handles_url_segment_with_RFC3986_special_chars(string segmentValue) { + // Test that URL segment parameters with RFC 3986 special characters don't get double-encoded + var client = new RestClient("https://api.example.com"); + var request = new RestRequest("path/{segment}/resource", Method.Get); + request.AddUrlSegment("segment", segmentValue); + + const string method = "GET"; + var url = client.BuildUri(request).ToString(); + var parameters = new WebPairCollection(); + + _workflow.RequestUrl = url; + var oauthParameters = _workflow.BuildProtectedResourceSignature(method, parameters); + + var signatureBase = OAuthTools.ConcatenateRequestElements(method, url, oauthParameters.Parameters); + + // The signature base should NOT contain double-encoded characters like %2521 (which is %25 + 21) + signatureBase.Should().NotContain("%25"); + + // But it should contain properly encoded special chars + signatureBase.Should().MatchRegex("%2[0-9A-F]"); + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests/Auth/OAuth1Tests.cs b/test/RestSharp.Tests/Auth/OAuth1Tests.cs new file mode 100644 index 000000000..483a89771 --- /dev/null +++ b/test/RestSharp.Tests/Auth/OAuth1Tests.cs @@ -0,0 +1,87 @@ +using System.Xml.Serialization; +using RestSharp.Authenticators; +using RestSharp.Authenticators.OAuth; +using RestSharp.Tests.Shared.Extensions; + +#pragma warning disable CS8618 + +namespace RestSharp.Tests.Auth; + +public class OAuth1Tests { + [XmlRoot("queue")] + class Queue { + [XmlElement("etag")] + public string Etag { get; set; } + + public List Items { get; set; } + } + + [XmlRoot("queue_item")] + class QueueItem { + [XmlElement("id")] + public string Id { get; set; } + + [XmlElement("position")] + public int Position { get; set; } + } + + [Fact] + public async Task Can_Authenticate_OAuth1_With_Querystring_Parameters() { + const string consumerKey = "enterConsumerKeyHere"; + const string consumerSecret = "enterConsumerSecretHere"; + const string baseUrl = "http://restsharp.org"; + + var expected = new List { + "oauth_consumer_key", + "oauth_nonce", + "oauth_signature_method", + "oauth_timestamp", + "oauth_version", + "oauth_signature" + }; + + using var client = new RestClient(baseUrl); + var request = new RestRequest(); + var authenticator = OAuth1Authenticator.ForRequestToken(consumerKey, consumerSecret); + authenticator.ParameterHandling = OAuthParameterHandling.UrlOrPostParameters; + await authenticator.Authenticate(client, request); + + var requestUri = client.BuildUri(request); + var actual = requestUri.ParseQuery().Select(x => x.Key).ToList(); + + actual.Should().BeEquivalentTo(expected); + } + + [Theory] + [MemberData(nameof(EncodeParametersTestData))] + public void Properly_Encodes_Parameter_Names(IList<(string, string)> parameters, string expected) { + var postData = new WebPairCollection(); + postData.AddRange(parameters.Select(x => new WebPair(x.Item1, x.Item2))); + var sortedParams = OAuthTools.SortParametersExcludingSignature(postData); + + sortedParams.First().Should().Be(expected); + } + + public static IEnumerable EncodeParametersTestData => new List { + new object[] { + new List<(string, string)> { ("name[first]", "Chuck"), ("name[last]", "Testa") }, + "name%5Bfirst%5D=Chuck" + }, + new object[] { + new List<(string, string)> { ("country", "España") }, + "country=Espa%C3%B1a" + } + }; + + [Fact] + public void Encodes_parameter() { + var parameter = new WebPair("status", "Hello Ladies + Gentlemen, a signed OAuth request!"); + var parameters = new WebPairCollection { parameter }; + + const string expected = "status%3DHello%2520Ladies%2520%252B%2520Gentlemen%252C%2520a%2520signed%2520OAuth%2520request%2521"; + + var norm = OAuthTools.NormalizeRequestParameters(parameters); + var escaped = OAuthTools.UrlEncodeRelaxed(norm); + escaped.Should().Be(expected); + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests/Auth/OAuthTests.cs b/test/RestSharp.Tests/Auth/OAuthTests.cs new file mode 100644 index 000000000..94e41010d --- /dev/null +++ b/test/RestSharp.Tests/Auth/OAuthTests.cs @@ -0,0 +1,95 @@ +using System.Globalization; +using System.Security.Cryptography; +using RestSharp.Authenticators.OAuth; +using RestSharp.Authenticators.OAuth.Extensions; + +namespace RestSharp.Tests.Auth; + +public class OAuthTests { + public OAuthTests() { + Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture; + Thread.CurrentThread.CurrentUICulture = CultureInfo.InstalledUICulture; + } + + [Fact] + public void HmacSha256_Does_Not_Accept_Nulls() { + const string consumerSecret = "12345678"; + + Assert.Throws( + () => OAuthTools.GetSignature(OAuthSignatureMethod.HmacSha256, null, consumerSecret) + ); + } + + [Theory] + [InlineData( + "The quick brown fox jumps over the lazy dog", + "rVL90tHhGt0eQ0TCITY74nVL22P%2FltlWS7WvJXpECPs%3D", + "12345678" + )] + [InlineData( + "The quick\tbrown\nfox\rjumps\r\nover\t\tthe\n\nlazy\r\n\r\ndog", + "C%2B2RY0Hna6VrfK1crCkU%2FV1e0ECoxoDh41iOOdmEMx8%3D", + "12345678" + )] + [InlineData("", "%2BnkCwZfv%2FQVmBbNZsPKbBT3kAg3JtVn3f3YMBtV83L8%3D", "12345678")] + [InlineData(" !\"#$%&'()*+,", "xcTgWGBVZaw%2Bilg6kjWAGt%2FhCcsVBMMe1CcDEnxnh8Y%3D", "12345678")] + [InlineData("AB", "JJgraAxzpO2Q6wiC3blM4eiQeA9WmkALaZI8yGRH4qM%3D", "CD!")] + public void HmacSha256_Hashes_Correctly(string value, string expected, string consumerSecret) { + var actual = OAuthTools.GetSignature(OAuthSignatureMethod.HmacSha256, value, consumerSecret); + + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData("1234", "%31%32%33%34")] + [InlineData("\x00\x01\x02\x03", "%00%01%02%03")] + [InlineData("\r\n\t", "%0D%0A%09")] + public void PercentEncode_Encodes_Correctly(string value, string expected) { + var actual = value.PercentEncode(); + + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData("The quick brown fox jumps over the lazy dog", 1024)] + [InlineData("The quick brown fox jumps over the lazy dog", 2048)] + [InlineData("The quick brown fox jumps over the lazy dog", 4096)] + [InlineData("", 2048)] + [InlineData(" !\"#$%&'()*+,", 2048)] + public void RsaSha1_Signs_Correctly(string value, int keySize) { + var hasher = SHA1.Create(); + var hash = hasher.ComputeHash(value.GetBytes()); + + using var crypto = new RSACryptoServiceProvider(keySize); + crypto.PersistKeyInCsp = false; + + var privateKey = crypto.ToXmlString(true); + + var signature = OAuthTools.GetSignature( + OAuthSignatureMethod.RsaSha1, + OAuthSignatureTreatment.Unescaped, + value, + privateKey + ); + + var signatureBytes = Convert.FromBase64String(signature); + + Assert.True(crypto.VerifyHash(hash, CryptoConfig.MapNameToOID("SHA1"), signatureBytes)); + } + + [Theory] + [InlineData("abcdefghijklmnopqrstuvwxyz", "abcdefghijklmnopqrstuvwxyz")] + [InlineData("ABCDEFGHIJKLMNOPQRSTUVWXYZ", "ABCDEFGHIJKLMNOPQRSTUVWXYZ")] + [InlineData("0123456789", "0123456789")] + [InlineData("-._~", "-._~")] + [InlineData(" !\"#$%&'()*+,", "%20%21%22%23%24%25%26%27%28%29%2A%2B%2C")] + [InlineData("%$%", "%25%24%25")] + [InlineData("%", "%25")] + [InlineData("/:;<=>?@", "%2F%3A%3B%3C%3D%3E%3F%40")] + [InlineData("\x00\x01\a\b\f\n\r\t\v", "%00%01%07%08%0C%0A%0D%09%0B")] + public void UrlStrictEncode_Encodes_Correctly(string value, string expected) { + var actual = OAuthTools.UrlEncodeStrict(value); + + Assert.Equal(expected, actual); + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests/Fixtures/MockHttpClient.cs b/test/RestSharp.Tests/Fixtures/MockHttpClient.cs new file mode 100644 index 000000000..4de8c1a24 --- /dev/null +++ b/test/RestSharp.Tests/Fixtures/MockHttpClient.cs @@ -0,0 +1,18 @@ +using RichardSzalay.MockHttp; + +namespace RestSharp.Tests.Fixtures; + +public static class MockHttpClient { + const string Url = "https://dummy.org"; + + public static RestClient Create(Method method, Func configureHandler, ConfigureRestClient configure = null) { + var mockHttp = new MockHttpMessageHandler(); + configureHandler(mockHttp.When(RestClient.AsHttpMethod(method), Url)); + + var options = new RestClientOptions(Url) { + ConfigureMessageHandler = _ => mockHttp + }; + configure?.Invoke(options); + return new RestClient(options); + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests/Headers/DefaultHeaderTests.cs b/test/RestSharp.Tests/Headers/DefaultHeaderTests.cs new file mode 100644 index 000000000..dd1e69c42 --- /dev/null +++ b/test/RestSharp.Tests/Headers/DefaultHeaderTests.cs @@ -0,0 +1,23 @@ +namespace RestSharp.Tests.Headers; + +public class DefaultHeaderTests { + const string BaseUrl = "http://localhost:8888/"; + + [Fact] + public void AddDefaultHeadersUsingDictionary() { + var headers = new Dictionary { + { KnownHeaders.ContentType, ContentType.Json }, + { KnownHeaders.Accept, ContentType.Json }, + { KnownHeaders.ContentEncoding, "gzip, deflate" } + }; + + var expected = headers.Select(x => new HeaderParameter(x.Key, x.Value)); + + using var client = new RestClient(BaseUrl); + client.AddDefaultHeaders(headers); + + var actual = client.DefaultParameters.Select(x => x as HeaderParameter); + expected.Should().BeSubsetOf(actual); + } + +} \ No newline at end of file diff --git a/test/RestSharp.Tests/Headers/HeaderRangeTests.cs b/test/RestSharp.Tests/Headers/HeaderRangeTests.cs new file mode 100644 index 000000000..ccedaa280 --- /dev/null +++ b/test/RestSharp.Tests/Headers/HeaderRangeTests.cs @@ -0,0 +1,23 @@ +namespace RestSharp.Tests.Parameters; + +public class HeaderRangeTests { + [Fact] + public async Task ShouldParseOutLongRangeSpecifier() { + using var restClient = new RestClient("http://localhost"); + var req = new RestRequest("bob"); + const long start = (long)int.MaxValue + 1; + const long end = start + 1; + + req.AddHeader("Range", $"pages={start}-{end}"); + await restClient.ExecuteAsync(req); + } + + [Fact] + public async Task ShouldParseOutRangeSpecifier() { + using var restClient = new RestClient("http://localhost"); + var req = new RestRequest("bob"); + + req.AddHeader("Range", "pages=1-2"); + await restClient.ExecuteAsync(req); + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests/Headers/HostHeaderTests.cs b/test/RestSharp.Tests/Headers/HostHeaderTests.cs new file mode 100644 index 000000000..4f16e6a12 --- /dev/null +++ b/test/RestSharp.Tests/Headers/HostHeaderTests.cs @@ -0,0 +1,50 @@ +namespace RestSharp.Tests; + +public class HostHeaderTests { + [Fact] + public void Cannot_Set_Empty_Host_Header() { + var request = new RestRequest(); + var exception = Assert.Throws(() => request.AddHeader("Host", string.Empty)); + + Assert.Equal("value", exception.ParamName); + } + + [Theory] + [InlineData("http://localhost")] + [InlineData("hostname 1234")] + [InlineData("-leading.hyphen.not.allowed")] + [InlineData("bad:port")] + [InlineData(" no.leading.white-space")] + [InlineData("no.trailing.white-space ")] + [InlineData(".leading.dot.not.allowed")] + [InlineData("double.dots..not.allowed")] + [InlineData(".")] + [InlineData(".:2345")] + [InlineData(":5678")] + [InlineData("")] + [InlineData("foo:bar:baz")] + public void Cannot_Set_Invalid_Host_Header(string value) { + var request = new RestRequest(); + var exception = Assert.Throws(() => request.AddHeader("Host", value)); + + Assert.Equal("value", exception.ParamName); + } + + [Theory] + [InlineData("localhost")] + [InlineData("localhost:1234")] + [InlineData("host.local")] + [InlineData("anotherhost.local:2345")] + [InlineData("www.w3.org")] + [InlineData("www.w3.org:3456")] + [InlineData("8.8.8.8")] + [InlineData("a.1.b.2")] + [InlineData("10.20.30.40:1234")] + [InlineData("0host")] + [InlineData("hypenated-hostname")] + [InlineData("multi--hyphens")] + public void Can_Set_Valid_Host_Header(string value) { + var request = new RestRequest(); + request.AddHeader("Host", value); + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests/Headers/RequestHeaderTests.cs b/test/RestSharp.Tests/Headers/RequestHeaderTests.cs new file mode 100644 index 000000000..bf8bc4c4b --- /dev/null +++ b/test/RestSharp.Tests/Headers/RequestHeaderTests.cs @@ -0,0 +1,187 @@ +namespace RestSharp.Tests; + +public class RequestHeaderTests { + static readonly KeyValuePair AcceptHeader = new(KnownHeaders.Accept, ContentType.Json); + static readonly KeyValuePair AcceptLanguageHeader = new(KnownHeaders.AcceptLanguage, "en-us,en;q=0.5"); + static readonly KeyValuePair KeepAliveHeader = new(KnownHeaders.KeepAlive, "300"); + + readonly List> _headers = [AcceptHeader, AcceptLanguageHeader, KeepAliveHeader]; + + [Fact] + public void AddHeaders_SameCaseDuplicatesExist_ThrowsException() { + var headers = _headers; + _headers.Add(AcceptHeader); + + var request = new RestRequest(); + + var exception = Assert.Throws(() => request.AddHeaders(headers)); + Assert.Equal("Duplicate header names exist: ACCEPT", exception.Message); + } + + [Fact] + public void AddHeaders_DifferentCaseDuplicatesExist_ThrowsException() { + var headers = _headers; + headers.Add(new(KnownHeaders.Accept, ContentType.Json)); + + var request = new RestRequest(); + + var exception = Assert.Throws(() => request.AddHeaders(headers)); + Assert.Equal("Duplicate header names exist: ACCEPT", exception.Message); + } + + [Fact] + public void AddHeaders_NoDuplicatesExist_Has3Headers() { + var request = new RestRequest(); + request.AddHeaders(_headers); + + var httpParameters = GetHeaders(request); + Assert.Equal(3, httpParameters.Length); + } + + [Fact] + public void AddHeaders_NoDuplicatesExistUsingDictionary_Has3Headers() { + var headers = new Dictionary { + { KnownHeaders.Accept, ContentType.Json }, + { KnownHeaders.AcceptLanguage, "en-us,en;q=0.5" }, + { KnownHeaders.KeepAlive, "300" } + }; + + var request = new RestRequest(); + request.AddHeaders(headers); + + var httpParameters = GetHeaders(request); + Assert.Equal(3, httpParameters.Count()); + } + + [Fact] + public void AddOrUpdateHeader_ShouldUpdateExistingHeader_WhenHeaderExist() { + // Arrange + var request = new RestRequest(); + request.AddHeader(KnownHeaders.Accept, ContentType.Xml); + + // Act + request.AddOrUpdateHeader(KnownHeaders.Accept, ContentType.Json); + + // Assert + GetHeader(request, KnownHeaders.Accept).Should().Be(ContentType.Json); + } + + [Fact] + public void AddOrUpdateHeader_ShouldUpdateExistingHeader_WhenHeaderDoesNotExist() { + // Arrange + var request = new RestRequest(); + + // Act + request.AddOrUpdateHeader(KnownHeaders.Accept, ContentType.Json); + + // Assert + GetHeader(request, KnownHeaders.Accept).Should().Be(ContentType.Json); + } + + [Fact] + public void AddOrUpdateHeaders_ShouldAddHeaders_WhenNoneExists() { + // Arrange + var headers = new Dictionary { + { KnownHeaders.Accept, ContentType.Json }, + { KnownHeaders.AcceptLanguage, "en-us,en;q=0.5" }, + { KnownHeaders.KeepAlive, "300" } + }; + + var request = new RestRequest(); + + // Act + request.AddOrUpdateHeaders(headers); + + // Assert + var requestHeaders = GetHeaders(request); + + var expected = headers.Select(x => new HeaderParameter(x.Key, x.Value)); + expected.Should().BeEquivalentTo(requestHeaders); + } + + [Fact] + public void AddOrUpdateHeaders_ShouldUpdateHeaders_WhenAllExists() { + // Arrange + var headers = new Dictionary { + { KnownHeaders.Accept, ContentType.Json }, + { KnownHeaders.KeepAlive, "300" } + }; + + var updatedHeaders = new Dictionary { + { KnownHeaders.Accept, ContentType.Xml }, + { KnownHeaders.KeepAlive, "400" } + }; + + var request = new RestRequest(); + request.AddHeaders(headers); + + // Act + request.AddOrUpdateHeaders(updatedHeaders); + + // Assert + var requestHeaders = GetHeaders(request); + + HeaderParameter[] expected = [new(KnownHeaders.Accept, ContentType.Xml), new(KnownHeaders.KeepAlive, "400")]; + requestHeaders.Should().BeEquivalentTo(expected); + } + + [Fact] + public void AddOrUpdateHeaders_ShouldAddAndUpdateHeaders_WhenSomeExists() { + // Arrange + var headers = new Dictionary { + { KnownHeaders.Accept, ContentType.Json }, + { KnownHeaders.KeepAlive, "300" } + }; + + var updatedHeaders = new Dictionary { + { KnownHeaders.Accept, ContentType.Xml }, + { KnownHeaders.AcceptLanguage, "en-us,en;q=0.5" } + }; + + var request = new RestRequest(); + request.AddHeaders(headers); + + // Act + request.AddOrUpdateHeaders(updatedHeaders); + + // Assert + var requestHeaders = GetHeaders(request); + + HeaderParameter[] expected = [ + new(KnownHeaders.Accept, ContentType.Xml), + new(KnownHeaders.AcceptLanguage, "en-us,en;q=0.5"), + new(KnownHeaders.KeepAlive, "300") + ]; + requestHeaders.Should().BeEquivalentTo(expected); + } + + [Fact] + public void Should_not_allow_null_header_value() { + string value = null; + var request = new RestRequest(); + // ReSharper disable once AssignNullToNotNullAttribute + Assert.Throws("value", () => request.AddHeader("name", value)); + } + + [Fact] + public void Should_not_allow_null_header_name() { + var request = new RestRequest(); + Assert.Throws("name", () => request.AddHeader(null!, "value")); + } + + [Fact] + public void Should_not_allow_empty_header_name() { + var request = new RestRequest(); + Assert.Throws("name", () => request.AddHeader("", "value")); + } + + [Fact] + public void Should_not_allow_CRLF_in_header_value() { + var request = new RestRequest(); + Assert.Throws(() => request.AddHeader("name", "test\r\nUser-Agent: injected header!\r\n\r\nGET /smuggled HTTP/1.1\r\nHost: insert.some.site.here")); + } + + static Parameter[] GetHeaders(RestRequest request) => request.Parameters.Where(x => x.Type == ParameterType.HttpHeader).ToArray(); + + static string GetHeader(RestRequest request, string name) => request.Parameters.FirstOrDefault(x => x.Name == name)?.Value?.ToString(); +} \ No newline at end of file diff --git a/test/RestSharp.Tests/MultipartFormTests.cs b/test/RestSharp.Tests/MultipartFormTests.cs new file mode 100644 index 000000000..3e6d555ec --- /dev/null +++ b/test/RestSharp.Tests/MultipartFormTests.cs @@ -0,0 +1,34 @@ +using System.Net; +using RestSharp.Tests.Fixtures; +using RichardSzalay.MockHttp; + +namespace RestSharp.Tests; + +public class MultipartFormTests { + [Fact] + public async Task ShouldHaveJsonContentType() { + var jsonData = new { + Company = "Microsoft", + ZipCode = "LS339", + Country = "USA" + }; + + using var client = MockHttpClient.Create(Method.Post, req => req.With(CheckRequest).Respond(HttpStatusCode.OK)); + + var request = new RestRequest { + Method = Method.Post, + AlwaysMultipartFormData = true + }; + request.AddJsonBody(jsonData); + + var response = await client.ExecuteAsync(request); + response.StatusCode.Should().Be(HttpStatusCode.OK); + return; + + bool CheckRequest(HttpRequestMessage msg) { + if (msg.Content is not MultipartFormDataContent formDataContent) return false; + + return formDataContent.First().Headers.ContentType!.MediaType == ContentType.Json; + } + } +} diff --git a/test/RestSharp.Tests/ObjectParserTests.cs b/test/RestSharp.Tests/ObjectParserTests.cs new file mode 100644 index 000000000..6bcbef081 --- /dev/null +++ b/test/RestSharp.Tests/ObjectParserTests.cs @@ -0,0 +1,56 @@ +// ReSharper disable PropertyCanBeMadeInitOnly.Local +namespace RestSharp.Tests; + +public class ObjectParserTests { + [Fact] + public void ShouldUseRequestProperty() { + var now = DateTime.Now; + var dates = new[] { now, now.AddDays(1), now.AddDays(2) }; + + var request = new TestObject { + SomeData = "test", + SomeDate = now, + Plain = 123, + PlainArray = dates, + DatesArray = dates + }; + + var parsed = request.GetProperties().ToDictionary(x => x.Name, x => x.Value); + parsed["some_data"].Should().Be(request.SomeData); + parsed["SomeDate"].Should().Be(request.SomeDate.ToString("d")); + parsed["Plain"].Should().Be(request.Plain.ToString()); + // ReSharper disable once SpecifyACultureInStringConversionExplicitly + parsed["PlainArray"].Should().Be(string.Join(",", dates.Select(x => x.ToString()))); + parsed["dates"].Should().Be(string.Join(",", dates.Select(x => x.ToString("d")))); + } + + [Fact] + public void ShouldProduceMultipleParametersForArray() { + var request = new AnotherTestObject { + SomeIds = [1, 2, 3] + }; + var expected = request.SomeIds.Select(x => ("ids[]", x.ToString())); + var parsed = request.GetProperties().Select(x => (x.Name, x.Value)); + + parsed.Should().BeEquivalentTo(expected); + } + + class AnotherTestObject { + [RequestProperty(Name = "ids", ArrayQueryType = RequestArrayQueryType.ArrayParameters)] + public int[] SomeIds { get; set; } + } + + class TestObject { + [RequestProperty(Name = "some_data")] + public string SomeData { get; set; } + + [RequestProperty(Format = "d")] + public DateTime SomeDate { get; set; } + + [RequestProperty(Name = "dates", Format = "d")] + public DateTime[] DatesArray { get; set; } + + public int Plain { get; set; } + public DateTime[] PlainArray { get; set; } + } +} diff --git a/test/RestSharp.Tests/OptionsTests.cs b/test/RestSharp.Tests/OptionsTests.cs new file mode 100644 index 000000000..d1d8aa372 --- /dev/null +++ b/test/RestSharp.Tests/OptionsTests.cs @@ -0,0 +1,17 @@ +namespace RestSharp.Tests; + +public class OptionsTests { + [Fact] + public void Ensure_follow_redirect() { + var value = false; + var options = new RestClientOptions { FollowRedirects = true, ConfigureMessageHandler = Configure }; + using var _ = new RestClient(options); + value.Should().BeTrue(); + return; + + HttpMessageHandler Configure(HttpMessageHandler handler) { + value = (handler as HttpClientHandler)!.AllowAutoRedirect; + return handler; + } + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests/Parameters/ObjectParameterTests.ArrayData.cs b/test/RestSharp.Tests/Parameters/ObjectParameterTests.ArrayData.cs new file mode 100644 index 000000000..367395d9f --- /dev/null +++ b/test/RestSharp.Tests/Parameters/ObjectParameterTests.ArrayData.cs @@ -0,0 +1,5 @@ +namespace RestSharp.Tests.Parameters; + +public partial class ObjectParameterTests { + sealed record ArrayData([property: RequestProperty(ArrayQueryType = RequestArrayQueryType.ArrayParameters)] TEnumerable Array) where TEnumerable : notnull; +} diff --git a/test/RestSharp.Tests/Parameters/ObjectParameterTests.CsvData.cs b/test/RestSharp.Tests/Parameters/ObjectParameterTests.CsvData.cs new file mode 100644 index 000000000..07cb78dc1 --- /dev/null +++ b/test/RestSharp.Tests/Parameters/ObjectParameterTests.CsvData.cs @@ -0,0 +1,5 @@ +namespace RestSharp.Tests.Parameters; + +public partial class ObjectParameterTests { + sealed record CsvData([property: RequestProperty(ArrayQueryType = RequestArrayQueryType.CommaSeparated)] TEnumerable Csv) where TEnumerable : notnull; +} diff --git a/test/RestSharp.Tests/Parameters/ObjectParameterTests.FormattedData.cs b/test/RestSharp.Tests/Parameters/ObjectParameterTests.FormattedData.cs new file mode 100644 index 000000000..fb76f8405 --- /dev/null +++ b/test/RestSharp.Tests/Parameters/ObjectParameterTests.FormattedData.cs @@ -0,0 +1,5 @@ +namespace RestSharp.Tests.Parameters; + +public partial class ObjectParameterTests { + sealed record FormattedData([property: RequestProperty(Format = "hh:mm tt")] TDateTime FormattedParameter) where TDateTime : notnull; +} diff --git a/test/RestSharp.Tests/Parameters/ObjectParameterTests.NamedData.cs b/test/RestSharp.Tests/Parameters/ObjectParameterTests.NamedData.cs new file mode 100644 index 000000000..2a6c0f574 --- /dev/null +++ b/test/RestSharp.Tests/Parameters/ObjectParameterTests.NamedData.cs @@ -0,0 +1,5 @@ +namespace RestSharp.Tests.Parameters; + +public partial class ObjectParameterTests { + sealed record NamedData([property: RequestProperty(Name = "CustomName")] object NamedParameter); +} diff --git a/test/RestSharp.Tests/Parameters/ObjectParameterTests.cs b/test/RestSharp.Tests/Parameters/ObjectParameterTests.cs new file mode 100644 index 000000000..305427f90 --- /dev/null +++ b/test/RestSharp.Tests/Parameters/ObjectParameterTests.cs @@ -0,0 +1,732 @@ +using System.Collections; +using System.Globalization; + +namespace RestSharp.Tests.Parameters; + +public partial class ObjectParameterTests { + public ObjectParameterTests() => Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture; + + [Fact] + public void Can_Add_Object_with_IntegerArray_property() { + var request = new RestRequest(); + var items = new[] { 2, 3, 4 }; + request.AddObject(new { Items = items }); + request.Parameters.First().Should().Be(new GetOrPostParameter("Items", string.Join(",", items))); + } + + [Fact] + public void Can_Add_Object_Static_with_Integer_property() { + const int item = 1230; + var @object = new { Item = item }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Item), "1230")); + } + + [Fact] + public void Can_Add_Object_Static_with_Integer_as_Object_property() { + const int item = 1230; + var @object = new { Item = (object)item }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Item), "1230")); + } + + [Fact] + public void Can_Add_Object_Static_with_IntegerArray_property() { + var items = new[] { 1, 2, 3 }; + var @object = new { Items = items }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Items), "1,2,3")); + } + + [Fact] + public void Can_Add_Object_Static_with_IntegerArray_as_ObjectArray_property() { + var items = new object[] { 1, 2, 3 }; + var @object = new { Items = items }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Items), "1,2,3")); + } + + [Fact] + public void Can_Add_Object_Static_with_IntegerArray_as_Object_property() { + var items = new[] { 1, 2, 3 }; + var @object = new { Items = (object)items }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Items), "1,2,3")); + } + + [Fact] + public void Can_Add_Object_Static_with_IntegerArray_as_ObjectArray_as_Object_property() { + var items = new object[] { 1, 2, 3 }; + var @object = new { Items = (object)items }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Items), "1,2,3")); + } + + [Fact] + public void Can_Add_Object_Static_with_IntegerArray_as_Enumerable_property() { + var items = new[] { 1, 2, 3 }; + var @object = new { Items = (IEnumerable)items }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Items), "1,2,3")); + } + + [Fact] + public void Can_Add_Object_Static_with_IntegerArray_as_Enumerable_as_Object_property() { + IEnumerable items = new[] { 1, 2, 3 }; + var @object = new { Items = (object)items }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Items), "1,2,3")); + } + + [Fact] + public void Can_Add_Object_Static_with_String_property() { + const string item = "Hello world"; + var @object = new { Item = item }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Item), item)); + } + + [Fact] + public void Can_Add_Object_Static_with_String_as_Object_property() { + const string item = "Hello world"; + var @object = new { Item = (object)item }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Item), item)); + } + + [Fact] + public void Can_Add_Object_Static_with_StringArray_property() { + var items = new[] { "Hello", "world", "from", "C#" }; + var @object = new { Items = items }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Items), "Hello,world,from,C#")); + } + + [Fact] + public void Can_Add_Object_Static_with_StringArray_as_ObjectArray_property() { + var items = new object[] { "Hello", "world", "from", "C#" }; + var @object = new { Items = items }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Items), "Hello,world,from,C#")); + } + + [Fact] + public void Can_Add_Object_Static_with_StringArray_as_Object_property() { + var items = new[] { "Hello", "world", "from", "C#" }; + var @object = new { Items = (object)items }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Items), "Hello,world,from,C#")); + } + + [Fact] + public void Can_Add_Object_Static_with_StringArray_as_ObjectArray_as_Object_property() { + var items = new object[] { "Hello", "world", "from", "C#" }; + var @object = new { Items = (object)items }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Items), "Hello,world,from,C#")); + } + + [Fact] + public void Can_Add_Object_Static_with_StringArray_as_Enumerable_property() { + var items = new[] { "Hello", "world", "from", "C#" }; + var @object = new { Items = (IEnumerable)items }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Items), "Hello,world,from,C#")); + } + + [Fact] + public void Can_Add_Object_Static_with_StringArray_as_Enumerable_as_Object_property() { + IEnumerable items = new[] { "Hello", "world", "from", "C#" }; + var @object = new { Items = (object)items }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Items), "Hello,world,from,C#")); + } + + [Fact] + public void Can_Add_Object_Static_with_DateTime_property() { + var item = DateTime.Parse("09/08/2025 13:35:23"); + var @object = new { Item = item }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Item), "09/08/2025 13:35:23")); + } + + [Fact] + public void Can_Add_Object_Static_with_DateTime_as_Object_property() { + var item = DateTime.Parse("04/06/2006 19:56:44"); + var @object = new { Item = (object)item }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Item), "04/06/2006 19:56:44")); + } + + [Fact] + public void Can_Add_Object_Static_with_DateTimeArray_property() { + var items = new[] { DateTime.Parse("01/01/2023 00:00:00"), DateTime.Parse("02/03/2024 14:30:00") }; + var @object = new { Items = items }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Items), "01/01/2023 00:00:00,02/03/2024 14:30:00")); + } + + [Fact] + public void Can_Add_Object_Static_with_DateTimeArray_as_ObjectArray_property() { + var items = new object[] { DateTime.Parse("01/01/2023 00:00:00"), DateTime.Parse("02/03/2024 14:30:00") }; + var @object = new { Items = items }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Items), "01/01/2023 00:00:00,02/03/2024 14:30:00")); + } + + [Fact] + public void Can_Add_Object_Static_with_DateTimeArray_as_Object_property() { + var items = new[] { DateTime.Parse("01/01/2023 00:00:00"), DateTime.Parse("02/03/2024 14:30:00") }; + var @object = new { Items = (object)items }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Items), "01/01/2023 00:00:00,02/03/2024 14:30:00")); + } + + [Fact] + public void Can_Add_Object_Static_with_DateTimeArray_as_ObjectArray_as_Object_property() { + var items = new object[] { DateTime.Parse("01/01/2023 00:00:00"), DateTime.Parse("02/03/2024 14:30:00") }; + var @object = new { Items = (object)items }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Items), "01/01/2023 00:00:00,02/03/2024 14:30:00")); + } + + [Fact] + public void Can_Add_Object_Static_with_DateTimeArray_as_Enumerable_property() { + var items = new[] { DateTime.Parse("01/01/2023 00:00:00"), DateTime.Parse("02/03/2024 14:30:00") }; + var @object = new { Items = (IEnumerable)items }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Items), "01/01/2023 00:00:00,02/03/2024 14:30:00")); + } + + [Fact] + public void Can_Add_Object_Static_with_DateTimeArray_as_Enumerable_as_Object_property() { + IEnumerable items = new[] { DateTime.Parse("01/01/2023 00:00:00"), DateTime.Parse("02/03/2024 14:30:00") }; + var @object = new { Items = (object)items }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Items), "01/01/2023 00:00:00,02/03/2024 14:30:00")); + } + + [Fact] + public void Can_Add_Object_Static_with_ObjectArray_property() { + var items = new object[] { "Hello world", 120, DateTime.Parse("06/06/2006 17:49:21"), Guid.Parse("1970a57f-d7f8-45d7-a269-f20e329d9432") }; + var @object = new { Items = items }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Items), "Hello world,120,06/06/2006 17:49:21,1970a57f-d7f8-45d7-a269-f20e329d9432")); + } + + [Fact] + public void Can_Add_Object_Static_with_ObjectArray_as_Object_property() { + var items = new object[] { "Hello world", 120, DateTime.Parse("06/06/2006 17:49:21"), Guid.Parse("1970a57f-d7f8-45d7-a269-f20e329d9432") }; + var @object = new { Items = (object)items }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Items), "Hello world,120,06/06/2006 17:49:21,1970a57f-d7f8-45d7-a269-f20e329d9432")); + } + + [Fact] + public void Can_Add_Object_Static_with_ObjectArray_as_Enumerable_property() { + var items = new object[] { "Hello world", 120, DateTime.Parse("06/06/2006 17:49:21"), Guid.Parse("1970a57f-d7f8-45d7-a269-f20e329d9432") }; + var @object = new { Items = (IEnumerable)items }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Items), "Hello world,120,06/06/2006 17:49:21,1970a57f-d7f8-45d7-a269-f20e329d9432")); + } + + [Fact] + public void Can_Add_Object_Static_with_ObjectArray_as_Enumerable_as_Object_property() { + IEnumerable items = new object[] { "Hello world", 120, DateTime.Parse("06/06/2006 17:49:21"), Guid.Parse("1970a57f-d7f8-45d7-a269-f20e329d9432") }; + var @object = new { Items = (object)items }; + var request = new RestRequest().AddObjectStatic(@object); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(@object.Items), "Hello world,120,06/06/2006 17:49:21,1970a57f-d7f8-45d7-a269-f20e329d9432")); + } + + [Fact] + public void Can_Add_Object_Static_with_custom_property_name() { + var item = new object[] { "Hello world", Array.Empty() }; + var namedData = new NamedData(item); + var request = new RestRequest().AddObjectStatic(namedData); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter("CustomName", "Hello world,Guid[] Array")); + } + + [Fact] + public void Can_Add_Object_Static_with_DateTime_using_custom_property_format() { + var item = DateTime.Parse("05/02/2020 09:12:33"); + var namedData = new FormattedData(item); + var request = new RestRequest().AddObjectStatic(namedData); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(FormattedData.FormattedParameter), "09:12 AM")); + } + + [Fact] + public void Can_Add_Object_Static_with_DateTime_as_Object_using_custom_property_format() { + var item = DateTime.Parse("05/02/2020 09:12:33"); + var namedData = new FormattedData(item); + var request = new RestRequest().AddObjectStatic(namedData); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(FormattedData.FormattedParameter), "09:12 AM")); + } + + [Fact] + public void Can_Add_Object_Static_with_DateTimeArray_using_custom_property_format() { + var item = new[] { DateTime.Parse("03/03/2019 12:11:00"), DateTime.Parse("10/05/2049 10:12:53"), DateTime.Parse("04/06/2025 23:44:59") }; + var namedData = new FormattedData(item); + var request = new RestRequest().AddObjectStatic(namedData); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(FormattedData.FormattedParameter), "12:11 PM,10:12 AM,11:44 PM")); + } + + [Fact] + public void Can_Add_Object_Static_with_DateTimeArray_as_ObjectArray_using_custom_property_format() { + var item = new object[] { DateTime.Parse("03/03/2019 12:11:00"), DateTime.Parse("10/05/2049 10:12:53"), DateTime.Parse("04/06/2025 23:44:59") }; + var namedData = new FormattedData(item); + var request = new RestRequest().AddObjectStatic(namedData); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(FormattedData.FormattedParameter), "12:11 PM,10:12 AM,11:44 PM")); + } + + [Fact] + public void Can_Add_Object_Static_with_DateTimeArray_as_Object_using_custom_property_format() { + var item = new[] { DateTime.Parse("03/03/2019 12:11:00"), DateTime.Parse("10/05/2049 10:12:53"), DateTime.Parse("04/06/2025 23:44:59") }; + var namedData = new FormattedData(item); + var request = new RestRequest().AddObjectStatic(namedData); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(FormattedData.FormattedParameter), "12:11 PM,10:12 AM,11:44 PM")); + } + + [Fact] + public void Can_Add_Object_Static_with_DateTimeArray_as_ObjectArray_as_Object_using_custom_property_format() { + var item = new object[] { DateTime.Parse("03/03/2019 12:11:00"), DateTime.Parse("10/05/2049 10:12:53"), DateTime.Parse("04/06/2025 23:44:59") }; + var namedData = new FormattedData(item); + var request = new RestRequest().AddObjectStatic(namedData); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(FormattedData.FormattedParameter), "12:11 PM,10:12 AM,11:44 PM")); + } + + [Fact] + public void Can_Add_Object_Static_with_DateTimeArray_as_Enumerable_using_custom_property_format() { + var item = new[] { DateTime.Parse("03/03/2019 12:11:00"), DateTime.Parse("10/05/2049 10:12:53"), DateTime.Parse("04/06/2025 23:44:59") }; + var namedData = new FormattedData(item); + var request = new RestRequest().AddObjectStatic(namedData); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(FormattedData.FormattedParameter), "12:11 PM,10:12 AM,11:44 PM")); + } + + [Fact] + public void Can_Add_Object_Static_with_DateTimeArray_as_ObjectArray_as_Enumerable_using_custom_property_format() { + var item = new object[] { DateTime.Parse("03/03/2019 12:11:00"), DateTime.Parse("10/05/2049 10:12:53"), DateTime.Parse("04/06/2025 23:44:59") }; + var namedData = new FormattedData(item); + var request = new RestRequest().AddObjectStatic(namedData); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(FormattedData.FormattedParameter), "12:11 PM,10:12 AM,11:44 PM")); + } + + [Fact] + public void Can_Add_Object_Static_with_StringArray_using_Csv_format() { + var items = new[] { "Hello", "world", "from", ".NET" }; + var namedData = new CsvData(items); + var request = new RestRequest().AddObjectStatic(namedData); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(CsvData.Csv), "Hello,world,from,.NET")); + } + + [Fact] + public void Can_Add_Object_Static_with_StringArray_as_ObjectArray_using_Csv_format() { + var items = new object[] { "Hello", "world", "from", ".NET" }; + var namedData = new CsvData(items); + var request = new RestRequest().AddObjectStatic(namedData); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(CsvData.Csv), "Hello,world,from,.NET")); + } + + [Fact] + public void Can_Add_Object_Static_with_StringArray_as_Object_using_Csv_format() { + var items = new[] { "Hello", "world", "from", ".NET" }; + var namedData = new CsvData(items); + var request = new RestRequest().AddObjectStatic(namedData); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(CsvData.Csv), "Hello,world,from,.NET")); + } + + [Fact] + public void Can_Add_Object_Static_with_StringArray_as_ObjectArray_as_Object_using_Csv_format() { + var items = new object[] { "Hello", "world", "from", ".NET" }; + var namedData = new CsvData(items); + var request = new RestRequest().AddObjectStatic(namedData); + + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(CsvData.Csv), "Hello,world,from,.NET")); + } + + [Fact] + public void Can_Add_Object_Static_with_StringArray_using_Array_format() { + var items = new[] { "Hello", "world", "from", ".NET" }; + var namedData = new ArrayData(items); + var request = new RestRequest().AddObjectStatic(namedData); + + request + .Parameters + .Should() + .BeEquivalentTo(new[] { + new GetOrPostParameter($"{nameof(ArrayData.Array)}[]", "Hello"), + new GetOrPostParameter($"{nameof(ArrayData.Array)}[]", "world"), + new GetOrPostParameter($"{nameof(ArrayData.Array)}[]", "from"), + new GetOrPostParameter($"{nameof(ArrayData.Array)}[]", ".NET") + }); + } + + [Fact] + public void Can_Add_Object_Static_with_StringArray_as_ObjectArray_using_Array_format() { + var items = new object[] { "Hello", "world", "from", ".NET" }; + var namedData = new ArrayData(items); + var request = new RestRequest().AddObjectStatic(namedData); + + request + .Parameters + .Should() + .BeEquivalentTo(new[] { + new GetOrPostParameter($"{nameof(ArrayData.Array)}[]", "Hello"), + new GetOrPostParameter($"{nameof(ArrayData.Array)}[]", "world"), + new GetOrPostParameter($"{nameof(ArrayData.Array)}[]", "from"), + new GetOrPostParameter($"{nameof(ArrayData.Array)}[]", ".NET") + }); + } + + [Fact] + public void Can_Add_Object_Static_with_StringArray_as_Object_using_Array_format() { + var items = new[] { "Hello", "world", "from", ".NET" }; + var namedData = new ArrayData(items); + var request = new RestRequest().AddObjectStatic(namedData); + + request + .Parameters + .Should() + .BeEquivalentTo(new[] { + new GetOrPostParameter($"{nameof(ArrayData.Array)}[]", "Hello"), + new GetOrPostParameter($"{nameof(ArrayData.Array)}[]", "world"), + new GetOrPostParameter($"{nameof(ArrayData.Array)}[]", "from"), + new GetOrPostParameter($"{nameof(ArrayData.Array)}[]", ".NET") + }); + } + + [Fact] + public void Can_Add_Object_Static_with_StringArray_as_ObjectArray_as_Object_using_Array_format() { + var items = new object[] { "Hello", "world", "from", ".NET" }; + var namedData = new ArrayData(items); + var request = new RestRequest().AddObjectStatic(namedData); + + request + .Parameters + .Should() + .BeEquivalentTo(new[] { + new GetOrPostParameter($"{nameof(ArrayData.Array)}[]", "Hello"), + new GetOrPostParameter($"{nameof(ArrayData.Array)}[]", "world"), + new GetOrPostParameter($"{nameof(ArrayData.Array)}[]", "from"), + new GetOrPostParameter($"{nameof(ArrayData.Array)}[]", ".NET") + }); + } + + [Fact] + public void RefStructs_are_ignored() { + const string value = "Hello world"; + var stringValue = new StringValue(value); + var request = new RestRequest().AddObjectStatic(stringValue); + request + .Parameters + .Should() + .ContainSingle() + .Which + .Should() + .BeEquivalentTo(new GetOrPostParameter(nameof(StringValue.Value), value)); + } + + [Fact] + public void Properties_are_filtered() { + var @object = new { Name = "Hello world", Age = 12, Guid = Guid.Parse("72df165c-0cef-4654-987f-cd844f1e5ce9"), Ignore = "Ignored" }; + var request = new RestRequest().AddObjectStatic(@object, nameof(@object.Name), nameof(@object.Age), nameof(@object.Guid)); + request + .Parameters + .Should() + .BeEquivalentTo(new[] { + new GetOrPostParameter(nameof(@object.Name), "Hello world"), + new GetOrPostParameter(nameof(@object.Age), "12"), + new GetOrPostParameter(nameof(@object.Guid), "72df165c-0cef-4654-987f-cd844f1e5ce9") + }); + } + + public sealed record StringValue(string Value) { + // ReSharper disable once UnusedMember.Global + public ReadOnlySpan AsSpan => Value.AsSpan(); + } +} diff --git a/test/RestSharp.Tests/Parameters/ParameterValidationTests.cs b/test/RestSharp.Tests/Parameters/ParameterValidationTests.cs new file mode 100644 index 000000000..81b6f39a7 --- /dev/null +++ b/test/RestSharp.Tests/Parameters/ParameterValidationTests.cs @@ -0,0 +1,42 @@ +namespace RestSharp.Tests.Parameters; + +public class ParameterValidationTests { + [Fact] + public void RestRequest_AlwaysMultipartFormData_IsAllowed() { + var request = new RestRequest { AlwaysMultipartFormData = true }; + request.ValidateParameters(); + } + + [Fact] + public void RestRequest_AlwaysSingleFileAsContent_IsAllowed() { + var request = new RestRequest { AlwaysSingleFileAsContent = true }; + request.ValidateParameters(); + } + + [Fact] + public void RestRequest_AlwaysSingleFileAsContent_And_AlwaysMultipartFormData_IsNotAllowed() { + var request = new RestRequest { + AlwaysSingleFileAsContent = true, + AlwaysMultipartFormData = true + }; + Assert.Throws(() => request.ValidateParameters()); + } + + [Fact] + public void RestRequest_AlwaysSingleFileAsContent_And_PostParameters_IsNotAllowed() { + var request = new RestRequest { + Method = Method.Post, + AlwaysSingleFileAsContent = true + }; + + request.AddParameter("name", "value", ParameterType.GetOrPost); + Assert.Throws(() => request.ValidateParameters()); + } + + [Fact] + public void RestRequest_AlwaysSingleFileAsContent_And_BodyParameters_IsNotAllowed() { + var request = new RestRequest { AlwaysSingleFileAsContent = true }; + request.AddParameter("name", "value", ParameterType.RequestBody); + Assert.Throws(() => request.ValidateParameters()); + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests/Parameters/UrlSegmentTests.cs b/test/RestSharp.Tests/Parameters/UrlSegmentTests.cs new file mode 100644 index 000000000..5282dc57c --- /dev/null +++ b/test/RestSharp.Tests/Parameters/UrlSegmentTests.cs @@ -0,0 +1,86 @@ +namespace RestSharp.Tests.Parameters; + +public class UrlSegmentTests { + const string BaseUrl = "http://localhost:8888/"; + + [Fact] + public void AddUrlSegmentWithInt() { + const string name = "foo"; + + var request = new RestRequest().AddUrlSegment(name, 1); + var actual = request.Parameters.FirstOrDefault(x => x.Name == name); + var expected = new UrlSegmentParameter(name, "1"); + + expected.Should().BeEquivalentTo(actual); + } + + [Fact] + public void AddUrlSegmentModifiesUrlSegmentWithInt() { + const string name = "foo"; + const string pathTemplate = "/{0}/resource"; + const int urlSegmentValue = 1; + + var path = string.Format(pathTemplate, $"{{{name}}}"); + var request = new RestRequest(path).AddUrlSegment(name, urlSegmentValue); + var expected = string.Format(pathTemplate, urlSegmentValue); + + using var client = new RestClient(BaseUrl); + var actual = client.BuildUri(request).AbsolutePath; + + expected.Should().BeEquivalentTo(actual); + } + + [Fact] + public void AddUrlSegmentModifiesUrlSegmentWithString() { + const string name = "foo"; + const string pathTemplate = "/{0}/resource"; + const string urlSegmentValue = "bar"; + + var path = string.Format(pathTemplate, $"{{{name}}}"); + var request = new RestRequest(path).AddUrlSegment(name, urlSegmentValue); + var expected = string.Format(pathTemplate, urlSegmentValue); + + using var client = new RestClient(BaseUrl); + + var actual = client.BuildUri(request).AbsolutePath; + + expected.Should().BeEquivalentTo(actual); + } + + [Theory] + [InlineData("bar%2fBAR")] + [InlineData("bar%2FBAR")] + public void UrlSegmentParameter_WithValueWithEncodedSlash_WillReplaceEncodedSlashByDefault(string inputValue) { + var urlSegmentParameter = new UrlSegmentParameter("foo", inputValue); + urlSegmentParameter.Value.Should().BeEquivalentTo("bar/BAR"); + } + + [Theory] + [InlineData("bar%2fBAR")] + [InlineData("bar%2FBAR")] + public void UrlSegmentParameter_WithValueWithEncodedSlash_CanReplaceEncodedSlash(string inputValue) { + var urlSegmentParameter = new UrlSegmentParameter("foo", inputValue, replaceEncodedSlash: true); + urlSegmentParameter.Value.Should().BeEquivalentTo("bar/BAR"); + } + + [Theory] + [InlineData("bar%2fBAR")] + [InlineData("bar%2FBAR")] + public void UrlSegmentParameter_WithValueWithEncodedSlash_CanLeaveEncodedSlash(string inputValue) { + var urlSegmentParameter = new UrlSegmentParameter("foo", inputValue, replaceEncodedSlash: false); + urlSegmentParameter.Value.Should().BeEquivalentTo(inputValue); + } + + [Fact] + public void AddSameUrlSegmentTwice_ShouldReplaceFirst() { + var client = new RestClient(); + var request = new RestRequest("https://api.example.com/orgs/{segment}/something"); + request.AddUrlSegment("segment", 1); + var url1 = client.BuildUri(request); + request.AddUrlSegment("segment", 2); + var url2 = client.BuildUri(request); + + url1.AbsolutePath.Should().Be("/orgs/1/something"); + url2.AbsolutePath.Should().Be("/orgs/2/something"); + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests/RestClientTests.cs b/test/RestSharp.Tests/RestClientTests.cs new file mode 100644 index 000000000..0bb54b328 --- /dev/null +++ b/test/RestSharp.Tests/RestClientTests.cs @@ -0,0 +1,126 @@ +using RestSharp.Serializers; +using RestSharp.Serializers.Json; + +namespace RestSharp.Tests; + +public class RestClientTests { + const string BaseUrl = "http://localhost:8888/"; + + [Theory] + [InlineData(Method.Get, Method.Post)] + [InlineData(Method.Post, Method.Get)] + [InlineData(Method.Delete, Method.Get)] + [InlineData(Method.Head, Method.Post)] + [InlineData(Method.Put, Method.Patch)] + [InlineData(Method.Patch, Method.Put)] + [InlineData(Method.Post, Method.Put)] + [InlineData(Method.Get, Method.Delete)] + public async Task Execute_with_RestRequest_and_Method_overrides_previous_request_method(Method reqMethod, Method overrideMethod) { + var req = new RestRequest("", reqMethod); + + using var client = new RestClient(BaseUrl); + await client.ExecuteAsync(req, overrideMethod); + + req.Method.Should().Be(overrideMethod); + } + + [Fact] + public async Task ConfigureHttp_will_set_proxy_to_null_with_no_exceptions_When_no_proxy_can_be_found() { + var req = new RestRequest(); + + using var client = new RestClient(new RestClientOptions(BaseUrl) { Proxy = null }); + await client.ExecuteAsync(req); + } + + [Fact] + public void UseJson_leaves_only_json_serializer() { + // arrange + var baseUrl = new Uri(BaseUrl); + + // act + using var client = new RestClient(baseUrl, configureSerialization: cfg => cfg.UseJson()); + + // assert + client.Serializers.Serializers.Should().HaveCount(1); + client.Serializers.GetSerializer(DataFormat.Json).Should().NotBeNull(); + } + + [Fact] + public void UseXml_leaves_only_json_serializer() { + // arrange + var baseUrl = new Uri(BaseUrl); + + // act + using var client = new RestClient(baseUrl, configureSerialization: cfg => cfg.UseXml()); + + // assert + client.Serializers.Serializers.Should().HaveCount(1); + client.Serializers.GetSerializer(DataFormat.Xml).Should().NotBeNull(); + } + + [Fact] + public void UseOnlySerializer_leaves_only_custom_serializer() { + // arrange + var baseUrl = new Uri(BaseUrl); + + // act + using var client = new RestClient(baseUrl, configureSerialization: cfg => cfg.UseOnlySerializer(() => new SystemTextJsonSerializer())); + + // assert + client.Serializers.Serializers.Should().HaveCount(1); + client.Serializers.GetSerializer(DataFormat.Json).Should().NotBeNull(); + } + + [Fact] + public void Should_reuse_httpClient_instance() { + using var client1 = new RestClient(new Uri("https://fake.api"), useClientFactory: true); + using var client2 = new RestClient(new Uri("https://fake.api"), useClientFactory: true); + + client1.HttpClient.Should().BeSameAs(client2.HttpClient); + } + + [Fact] + public void Should_use_new_httpClient_instance() { + using var client1 = new RestClient(new Uri("https://fake.api")); + using var client2 = new RestClient(new Uri("https://fake.api")); + + client1.HttpClient.Should().NotBeSameAs(client2.HttpClient); + } + + [Fact] + public void ConfigureDefaultParameters_sets_user_agent_new_httpClient_instance() { + // arrange + var clientOptions = new RestClientOptions(); + + // act + using var restClient = new RestClient(clientOptions); + + //assert + Assert.Single( + restClient.DefaultParameters, + parameter => parameter is { Type: ParameterType.HttpHeader, Name: KnownHeaders.UserAgent, Value: string valueAsString } && + valueAsString == clientOptions.UserAgent + ); + + Assert.Empty(restClient.HttpClient.DefaultRequestHeaders.UserAgent); + } + + [Fact] + public void ConfigureDefaultParameters_sets_user_agent_given_httpClient_instance() { + // arrange + var httpClient = new HttpClient(); + var clientOptions = new RestClientOptions(); + + // act + using var restClient = new RestClient(httpClient, clientOptions); + + //assert + Assert.Single( + restClient.DefaultParameters, + parameter => parameter is { Type: ParameterType.HttpHeader, Name: KnownHeaders.UserAgent, Value: string valueAsString } && + valueAsString == clientOptions.UserAgent + ); + + Assert.Empty(httpClient.DefaultRequestHeaders.UserAgent); + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests/RestContentTests.cs b/test/RestSharp.Tests/RestContentTests.cs new file mode 100644 index 000000000..8f1a97343 --- /dev/null +++ b/test/RestSharp.Tests/RestContentTests.cs @@ -0,0 +1,28 @@ +namespace RestSharp.Tests; + +public class RestContentTests { + [Fact] + public void RestContent_CaseInsensitiveHeaders() { + const string myContentType = "application/x-custom"; + + var request = new RestRequest("resource").AddHeader("coNteNt-TypE", myContentType); + var content = new RequestContent(new RestClient(), request); + + var httpContent = content.BuildContent(); + + httpContent.Headers.ContentType!.MediaType.Should().Be(myContentType); + } + + [Fact] + public void RestContent_supports_manual_json_body() { + string myContentType = ContentType.Json; + const string myJsonString = "[]"; + + var request = new RestRequest("resource").AddParameter(myContentType, myJsonString, ParameterType.RequestBody); + var content = new RequestContent(new RestClient(), request); + + var httpContent = content.BuildContent(); + + httpContent.Headers.ContentType!.MediaType.Should().Be(myContentType); + } +} diff --git a/test/RestSharp.Tests/RestRequestTests.cs b/test/RestSharp.Tests/RestRequestTests.cs new file mode 100644 index 000000000..988f91734 --- /dev/null +++ b/test/RestSharp.Tests/RestRequestTests.cs @@ -0,0 +1,40 @@ +namespace RestSharp.Tests; + +public class RestRequestTests { + [Fact] + public void RestRequest_Request_Property() { + var request = new RestRequest("resource"); + request.Resource.Should().Be("resource"); + } + + [Fact] + public void RestRequest_Test_Already_Encoded() { + const string resource = "/api/get?query=Id%3d198&another=notencoded&novalue="; + const string baseUrl = "https://example.com"; + + var request = new RestRequest(resource); + var parameters = request.Parameters.ToArray(); + + request.Resource.Should().Be("/api/get"); + parameters.Length.Should().Be(3); + + var expected = new[] { + new { Name = "query", Value = "Id%3d198", Type = ParameterType.QueryString, Encode = false }, + new { Name = "another", Value = "notencoded", Type = ParameterType.QueryString, Encode = false }, + new { Name = "novalue", Value = "", Type = ParameterType.QueryString, Encode = false } + }; + parameters.Should().BeEquivalentTo(expected, options => options.ExcludingMissingMembers()); + + using var client = new RestClient(baseUrl); + var actual = client.BuildUri(request); + actual.AbsoluteUri.Should().Be($"{baseUrl}{resource}"); + } + + [Fact] + public async Task RestRequest_Fail_On_Exception() { + var req = new RestRequest("nonexisting"); + + using var client = new RestClient(new RestClientOptions("http://localhost:12345") { ThrowOnAnyError = true }); + await Assert.ThrowsAsync(() => client.ExecuteAsync(req)); + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests/RestSharp.Tests.csproj b/test/RestSharp.Tests/RestSharp.Tests.csproj new file mode 100644 index 000000000..af5fb6fa9 --- /dev/null +++ b/test/RestSharp.Tests/RestSharp.Tests.csproj @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + UrlBuilderTests.cs + + + UrlBuilderTests.cs + + + \ No newline at end of file diff --git a/RestSharp.Tests/SampleData/4sq.txt b/test/RestSharp.Tests/SampleData/4sq.json similarity index 100% rename from RestSharp.Tests/SampleData/4sq.txt rename to test/RestSharp.Tests/SampleData/4sq.json diff --git a/test/RestSharp.Tests/SampleData/GenericWithList.json b/test/RestSharp.Tests/SampleData/GenericWithList.json new file mode 100644 index 000000000..353cbad1a --- /dev/null +++ b/test/RestSharp.Tests/SampleData/GenericWithList.json @@ -0,0 +1,9 @@ +{ + "Data": + { + "Items": + [ + {"Nickname":"Foe sho"} + ] + } +} diff --git a/test/RestSharp.Tests/SampleData/bearertoken.json b/test/RestSharp.Tests/SampleData/bearertoken.json new file mode 100644 index 000000000..aa5518857 --- /dev/null +++ b/test/RestSharp.Tests/SampleData/bearertoken.json @@ -0,0 +1,8 @@ +{ + "access_token":"boQtj0SCGz2GFGz[...]", + "token_type":"bearer", + "expires_in":1209599, + "userName":"Alice", + ".issued":"Mon, 14 Oct 2013 06:53:32 GMT", + ".expires":"Mon, 28 Oct 2013 06:53:32 GMT" +} \ No newline at end of file diff --git a/RestSharp.Tests/SampleData/datetimes.txt b/test/RestSharp.Tests/SampleData/datetimes.json similarity index 100% rename from RestSharp.Tests/SampleData/datetimes.txt rename to test/RestSharp.Tests/SampleData/datetimes.json diff --git a/RestSharp.Tests/SampleData/iso8601datetimes.txt b/test/RestSharp.Tests/SampleData/iso8601datetimes.json similarity index 100% rename from RestSharp.Tests/SampleData/iso8601datetimes.txt rename to test/RestSharp.Tests/SampleData/iso8601datetimes.json diff --git a/RestSharp.Tests/SampleData/jsonarray.txt b/test/RestSharp.Tests/SampleData/jsonarray.json similarity index 100% rename from RestSharp.Tests/SampleData/jsonarray.txt rename to test/RestSharp.Tests/SampleData/jsonarray.json diff --git a/test/RestSharp.Tests/SampleData/jsondictionary.json b/test/RestSharp.Tests/SampleData/jsondictionary.json new file mode 100644 index 000000000..c0196c326 --- /dev/null +++ b/test/RestSharp.Tests/SampleData/jsondictionary.json @@ -0,0 +1,21 @@ +{ + "EmployeesPay" : + { + "John": [{"Type":"BiWeekly","Amount":3000},{"Type":"Bonus","Amount":5000}], + "David": [{"Type":"Monthly","Amount":5000},{"Type":"Bonus","Amount":2500}], + "Mary": [{"Type":"BiWeekly","Amount":2000}] + }, + "EmployeesMail" : + { + "John": ["Welcome to Restsharp", "Meetings at 4pm", "Meeting Cancled"], + "David": ["Project deadline is Monday", "Good work"], + "Mary": ["Is there any documentation on Product A", "I'm leaving early today"] + }, + + "EmployeesTime" : + { + "John": [[8, 7, 8, 8, 8], [1, 2, 3]], + "David": [[4, 12, 6, 4],[4, 12, 6, 4]], + "Mary": [[]] + } +} diff --git a/test/RestSharp.Tests/SampleData/jsondictionary_KeysType.json b/test/RestSharp.Tests/SampleData/jsondictionary_KeysType.json new file mode 100644 index 000000000..aa2c517a3 --- /dev/null +++ b/test/RestSharp.Tests/SampleData/jsondictionary_KeysType.json @@ -0,0 +1,14 @@ +{ + "1" : + { + "John": [{"Type":"BiWeekly","Amount":3000},{"Type":"Bonus","Amount":5000}], + "David": [{"Type":"Monthly","Amount":5000},{"Type":"Bonus","Amount":2500}], + "Mary": [{"Type":"BiWeekly","Amount":2000}] + }, + "2" : + { + "John": ["Welcome to Restsharp", "Meetings at 4pm", "Meeting Cancled"], + "David": ["Project deadline is Monday", "Good work"], + "Mary": ["Is there any documentation on Product A", "I'm leaving early today"] + } +} diff --git a/test/RestSharp.Tests/SampleData/jsondictionary_null.json b/test/RestSharp.Tests/SampleData/jsondictionary_null.json new file mode 100644 index 000000000..a9b1e36c0 --- /dev/null +++ b/test/RestSharp.Tests/SampleData/jsondictionary_null.json @@ -0,0 +1,7 @@ +{ + "SomeDictionary" : + { + "NonNull": "abra", + "Null": null + } +} diff --git a/RestSharp.Tests/SampleData/jsonenums.txt b/test/RestSharp.Tests/SampleData/jsonenums.json similarity index 100% rename from RestSharp.Tests/SampleData/jsonenums.txt rename to test/RestSharp.Tests/SampleData/jsonenums.json diff --git a/RestSharp.Tests/SampleData/jsonenumtypes.txt b/test/RestSharp.Tests/SampleData/jsonenumtypes.json similarity index 100% rename from RestSharp.Tests/SampleData/jsonenumtypes.txt rename to test/RestSharp.Tests/SampleData/jsonenumtypes.json diff --git a/test/RestSharp.Tests/SampleData/jsonlists.json b/test/RestSharp.Tests/SampleData/jsonlists.json new file mode 100644 index 000000000..9646a0b7e --- /dev/null +++ b/test/RestSharp.Tests/SampleData/jsonlists.json @@ -0,0 +1,17 @@ +{ + "names" : [ + "John", + "Bob", + "Albert", + "Jeff", + "Charlie" + ], + "numbers" : [ + 1, + 2, + 3, + 5, + 7, + 11 + ] +} diff --git a/test/RestSharp.Tests/SampleData/newdatetimes.json b/test/RestSharp.Tests/SampleData/newdatetimes.json new file mode 100644 index 000000000..8c193c13b --- /dev/null +++ b/test/RestSharp.Tests/SampleData/newdatetimes.json @@ -0,0 +1,4 @@ +{ + "DateTime": "new Date(1309421746929)", + "DateTimeNegative": "new Date(-1)" +} \ No newline at end of file diff --git a/RestSharp.Tests/SampleData/objectproperty.txt b/test/RestSharp.Tests/SampleData/objectproperty.json similarity index 100% rename from RestSharp.Tests/SampleData/objectproperty.txt rename to test/RestSharp.Tests/SampleData/objectproperty.json diff --git a/RestSharp.Tests/SampleData/person.json.txt b/test/RestSharp.Tests/SampleData/person.json similarity index 100% rename from RestSharp.Tests/SampleData/person.json.txt rename to test/RestSharp.Tests/SampleData/person.json diff --git a/RestSharp.Tests/SampleData/sojson.txt b/test/RestSharp.Tests/SampleData/sojson.json similarity index 100% rename from RestSharp.Tests/SampleData/sojson.txt rename to test/RestSharp.Tests/SampleData/sojson.json diff --git a/test/RestSharp.Tests/SampleData/timespans.json b/test/RestSharp.Tests/SampleData/timespans.json new file mode 100644 index 000000000..7b8ea2071 --- /dev/null +++ b/test/RestSharp.Tests/SampleData/timespans.json @@ -0,0 +1,15 @@ +{ + "Tick": "00:00:00.0468006", + "Millisecond": "00:00:00.1250000", + "Second": "00:00:08.0000000", + "Minute": "00:55:02.0000000", + "Hour": "21:30:07.0000000", + "NullableWithoutValue": null, + "NullableWithValue": "21:30:07.0000000", + "IsoSecond": "PT10S", + "IsoMinute": "PT3M23S", + "IsoHour": "PT5H4M9S", + "IsoDay": "P1DT19H27M13S", + "IsoMonth": "P2M4DT3H14M19S", + "IsoYear": "P1YT9H27M48S" +} diff --git a/test/RestSharp.Tests/SampleData/underscore_prefix.json b/test/RestSharp.Tests/SampleData/underscore_prefix.json new file mode 100644 index 000000000..753421134 --- /dev/null +++ b/test/RestSharp.Tests/SampleData/underscore_prefix.json @@ -0,0 +1,18 @@ +{ + "User": { + "_id": 1786, + "_displayName": "John Sheehan", + "Reputation": 18332, + "CreationDate": 1219071163, + "_displayName": "John Sheehan", + "EmailHash": "lkdsafadfjsadfkjlsdjflkjdsf", + "Age": "28", + "LastAccessDate": 1269715453, + "WebsiteUrl": "http://john-sheehan.com/blog", + "Location": "Minnesota", + "AboutMe": "

Follow me on Twitter

\r\n\r\n

Read my blog

\r\n\r\n

Visit managedassembly.com - A community for .NET developers.

\r\n\r\n

I am the founder of RIM Systems, maker of SnapLeague, a web-based league management system for recreational athletics.

", + "Views": 1639, + "UpVotes": 1665, + "DownVotes": 427 + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests/StringExtensionsTests.cs b/test/RestSharp.Tests/StringExtensionsTests.cs new file mode 100644 index 000000000..045be6d9f --- /dev/null +++ b/test/RestSharp.Tests/StringExtensionsTests.cs @@ -0,0 +1,73 @@ +using System.Globalization; +using System.Text; +using RestSharp.Extensions; + +namespace RestSharp.Tests; + +public class StringExtensionsTests { + [Fact] + public void UrlEncode_Throws_ArgumentNullException_For_Null_Input() { + string nullString = null; + // ReSharper disable once ExpressionIsAlwaysNull + Assert.Throws(() => nullString!.UrlEncode()); + } + + [Fact] + public void UrlEncode_Returns_Correct_Length_When_Less_Than_Limit() { + const int numLessThanLimit = 32766; + var stringWithLimitLength = new string('*', numLessThanLimit); + var encodedAndDecoded = stringWithLimitLength.UrlEncode().UrlDecode(); + Assert.Equal(numLessThanLimit, encodedAndDecoded.Length); + } + + [Fact] + public void UrlEncode_Returns_Correct_Length_When_More_Than_Limit() { + const int numGreaterThanLimit = 65000; + var stringWithLimitLength = new string('*', numGreaterThanLimit); + var encodedAndDecoded = stringWithLimitLength.UrlEncode().UrlDecode(); + Assert.Equal(numGreaterThanLimit, encodedAndDecoded.Length); + } + + [Fact] + public void UrlEncode_Does_Not_Fail_When_4_Byte_Unicode_Character_Lies_Between_Chunks() { + var stringWithLimitLength = new string('*', 32765); + stringWithLimitLength += "😉*****"; // 2 + 5 chars + var encodedAndDecoded = stringWithLimitLength.UrlEncode().UrlDecode(); + Assert.Equal(stringWithLimitLength, encodedAndDecoded); + + // now between another 2 chunks + stringWithLimitLength = new string('*', 32766 * 2 - 1); + stringWithLimitLength += "😉*****"; // 2 + 5 chars + encodedAndDecoded = stringWithLimitLength.UrlEncode().UrlDecode(); + Assert.Equal(stringWithLimitLength, encodedAndDecoded); + } + + [Fact] + public void UrlEncodeTest() { + const string parameter = "ø"; + Assert.Equal("%F8", parameter.UrlEncode(Encoding.GetEncoding("ISO-8859-1")), true); + Assert.Equal("%C3%B8", parameter.UrlEncode(), true); + } + + [Theory] + [InlineData("this_is_a_test", true, "ThisIsATest")] + [InlineData("this_is_a_test", false, "This_Is_A_Test")] + public void ToPascalCase(string start, bool removeUnderscores, string finish) { + var result = start.ToPascalCase(removeUnderscores, CultureInfo.InvariantCulture); + + Assert.Equal(finish, result); + } + + [Theory] + [InlineData("DueDate", "dueDate")] + [InlineData("ID", "id")] + [InlineData("IDENTIFIER", "identifier")] + [InlineData("primaryId", "primaryId")] + [InlineData("A", "a")] + [InlineData("ThisIsATest", "thisIsATest")] + public void ToCamelCase(string start, string finish) { + var result = start.ToCamelCase(CultureInfo.InvariantCulture); + + Assert.Equal(finish, result); + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests/UrlBuilderTests.Get.cs b/test/RestSharp.Tests/UrlBuilderTests.Get.cs new file mode 100644 index 000000000..85c04b35e --- /dev/null +++ b/test/RestSharp.Tests/UrlBuilderTests.Get.cs @@ -0,0 +1,227 @@ +namespace RestSharp.Tests; + +public partial class UrlBuilderTests { + [Fact] + public void GET_with_empty_base_and_query_parameters_without_encoding() { + var request = new RestRequest($"{Base}/{Resource}?param1=value1") + .AddQueryParameter("foo", "bar,baz", false); + var expected = new Uri($"{Base}/{Resource}?param1=value1&foo=bar,baz"); + + using var client = new RestClient(); + + var output = client.BuildUri(request); + Assert.Equal(expected, output); + } + + [Fact] + public void GET_with_empty_base_and_resource_containing_tokens() { + var request = new RestRequest($"{Base}/{Resource}/{{foo}}"); + request.AddUrlSegment("foo", "bar"); + + using var client = new RestClient(); + + var expected = new Uri($"{Base}/{Resource}/bar"); + var output = client.BuildUri(request); + + Assert.Equal(expected, output); + } + + [Fact] + public void GET_with_empty_request() { + var request = new RestRequest(); + var expected = new Uri(Base); + + using var client = new RestClient(new Uri(Base)); + + var output = client.BuildUri(request); + Assert.Equal(expected, output); + } + + [Fact] + public void GET_with_empty_request_and_bare_hostname() { + var request = new RestRequest(); + var expected = new Uri(Base); + + using var client = new RestClient(new Uri(Base)); + + var output = client.BuildUri(request); + Assert.Equal(expected, output); + } + + [Fact] + public void GET_with_empty_request_and_query_parameters_without_encoding() { + var request = new RestRequest(); + request.AddQueryParameter("foo", "bar,baz", false); + var expected = new Uri($"{Base}/{Resource}?param1=value1&foo=bar,baz"); + + using var client = new RestClient($"{Base}/{Resource}?param1=value1"); + + var output = client.BuildUri(request); + Assert.Equal(expected, output); + } + + [Fact] + public void GET_with_Invalid_Url_string_throws_exception() + => Assert.Throws(() => { _ = new RestClient("invalid url"); } + ); + + [Fact] + public void GET_with_leading_slash() { + var request = new RestRequest($"/{Resource}"); + var expected = new Uri($"{Base}/{Resource}"); + + using var client = new RestClient(new Uri(Base)); + + var output = client.BuildUri(request); + Assert.Equal(expected, output); + } + + [Fact] + public void GET_with_leading_slash_and_baseurl_trailing_slash() { + var request = new RestRequest($"/{Resource}"); + request.AddParameter("foo", "bar"); + var expected = new Uri($"{Base}/{Resource}?foo=bar"); + + using var client = new RestClient(new Uri(Base)); + + var output = client.BuildUri(request); + Assert.Equal(expected, output); + } + + [Fact] + public void GET_with_multiple_instances_of_same_key() { + var request = new RestRequest("v1/people/~/network/updates"); + request.AddParameter("type", "STAT"); + request.AddParameter("type", "PICT"); + request.AddParameter("count", "50"); + request.AddParameter("start", "50"); + var expected = new Uri("https://api.linkedin.com/v1/people/~/network/updates?type=STAT&type=PICT&count=50&start=50"); + + using var client = new RestClient("https://api.linkedin.com"); + + var output = client.BuildUri(request); + Assert.Equal(expected, output); + } + + [Fact] + public void GET_with_resource_containing_null_token() { + var request = new RestRequest($"/{Resource}/{{foo}}"); + request.AddUrlSegment("foo", null); + + using var client = new RestClient(new Uri(Base)); + + var output = client.BuildUri(request); + Assert.Equal(new($"{Base}/{Resource}/"), output); + } + + [Fact] + public void GET_with_resource_containing_slashes() { + var request = new RestRequest($"{Resource}/foo"); + var expected = new Uri($"{Base}/{Resource}/foo"); + + using var client = new RestClient(new Uri(Base)); + + var output = client.BuildUri(request); + Assert.Equal(expected, output); + } + + [Fact] + public void GET_with_resource_containing_tokens() { + var request = new RestRequest($"{Resource}/{{foo}}"); + request.AddUrlSegment("foo", "bar"); + var expected = new Uri($"{Base}/{Resource}/bar"); + + using var client = new RestClient(new Uri(Base)); + + var output = client.BuildUri(request); + Assert.Equal(expected, output); + } + + [Fact] + public void GET_with_Uri_and_resource_containing_tokens() { + var request = new RestRequest($"/{{foo}}/{Resource}/{{baz}}"); + request.AddUrlSegment("foo", "bar"); + request.AddUrlSegment("baz", "bat"); + var expected = new Uri($"{Base}/bar/{Resource}/bat"); + + using var client = new RestClient(Base); + + var output = client.BuildUri(request); + Assert.Equal(expected, output); + } + + [Fact] + public void GET_with_Uri_containing_tokens() { + var request = new RestRequest(); + request.AddUrlSegment("foo", "bar"); + var expected = new Uri(Base); + + using var client = new RestClient(Base); + + var output = client.BuildUri(request); + Assert.Equal(expected, output); + } + + [Fact] + public void GET_with_Url_string_and_resource_containing_tokens() { + var request = new RestRequest($"{Resource}/{{baz}}"); + request.AddUrlSegment("foo", "bar"); + request.AddUrlSegment("baz", "bat"); + var expected = new Uri($"{Base}/bar/{Resource}/bat"); + + using var client = new RestClient($"{Base}/{{foo}}"); + + var output = client.BuildUri(request); + Assert.Equal(expected, output); + } + + [Fact] + public void GET_with_Url_string_containing_tokens() { + var request = new RestRequest(); + request.AddUrlSegment("foo", "bar"); + var expected = new Uri($"{Base}/bar"); + + using var client = new RestClient($"{Base}/{{foo}}"); + + var output = client.BuildUri(request); + Assert.Equal(expected, output); + } + + [Fact] + public void GET_wth_trailing_slash_and_query_parameters() { + var request = new RestRequest($"/{Resource}/"); + request.AddParameter("foo", "bar"); + var expected = new Uri($"{Base}/{Resource}/?foo=bar"); + + using var client = new RestClient(Base); + + var output = client.BuildUri(request); + Assert.Equal(expected, output); + } + + [Fact] + public void Multiple_query_parameters_with_same_name() { + var request = new RestRequest($"/{Resource}/") + .AddQueryParameter("foo", "bar") + .AddQueryParameter("foo", "baz"); + var expected = new Uri($"{Base}/{Resource}/?foo=bar&foo=baz"); + + using var client = new RestClient(Base); + + var output = client.BuildUri(request); + Assert.Equal(expected, output); + } + + [Fact] + public void Query_parameter_with_non_string_value() { + var request = new RestRequest($"/{Resource}/") + .AddQueryParameter("foo", 123) + .AddQueryParameter("bar", true); + var expected = new Uri($"{Base}/{Resource}/?foo=123&bar=True"); + + using var client = new RestClient(Base); + + var output = client.BuildUri(request); + Assert.Equal(expected, output); + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests/UrlBuilderTests.Post.cs b/test/RestSharp.Tests/UrlBuilderTests.Post.cs new file mode 100644 index 000000000..129b1079f --- /dev/null +++ b/test/RestSharp.Tests/UrlBuilderTests.Post.cs @@ -0,0 +1,60 @@ +namespace RestSharp.Tests; + +public partial class UrlBuilderTests { + [Fact] + public void POST_with_leading_slash() { + var request = new RestRequest($"/{Resource}", Method.Post); + var expected = new Uri($"{Base}/{Resource}"); + + using var client = new RestClient(new Uri(Base)); + + var output = client.BuildUri(request); + Assert.Equal(expected, output); + } + + [Fact] + public void POST_with_leading_slash_and_baseurl_trailing_slash() { + var request = new RestRequest($"/{Resource}", Method.Post); + var expected = new Uri($"{Base}/{Resource}"); + + using var client = new RestClient(Base); + + var output = client.BuildUri(request); + Assert.Equal(expected, output); + } + + [Fact] + public void POST_with_querystring_containing_tokens() { + var request = new RestRequest(Resource, Method.Post); + request.AddParameter("foo", "bar", ParameterType.QueryString); + var expected = new Uri($"{Base}/{Resource}?foo=bar"); + + using var client = new RestClient(Base); + + var output = client.BuildUri(request); + Assert.Equal(expected, output); + } + + [Fact] + public void POST_with_resource_containing_slashes() { + var request = new RestRequest($"{Resource}/foo", Method.Post); + var expected = new Uri($"{Base}/{Resource}/foo"); + + using var client = new RestClient(Base); + + var output = client.BuildUri(request); + Assert.Equal(expected, output); + } + + [Fact] + public void POST_with_resource_containing_tokens() { + var request = new RestRequest($"{Resource}/{{foo}}", Method.Post); + request.AddUrlSegment("foo", "bar"); + var expected = new Uri($"{Base}/{Resource}/bar"); + + using var client = new RestClient(Base); + + var output = client.BuildUri(request); + Assert.Equal(expected, output); + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests/UrlBuilderTests.cs b/test/RestSharp.Tests/UrlBuilderTests.cs new file mode 100644 index 000000000..9a80a20f6 --- /dev/null +++ b/test/RestSharp.Tests/UrlBuilderTests.cs @@ -0,0 +1,147 @@ +using System.Text; + +namespace RestSharp.Tests; + +/// +/// Note: These tests do not handle QueryString building, which is handled in Http, not RestClient +/// +public partial class UrlBuilderTests { + const string Base = "https://some.path"; + const string Resource = "resource"; + + [Fact] + public void Should_add_parameter_if_it_is_new() { + var request = new RestRequest(); + request.AddOrUpdateParameter("param2", "value2"); + request.AddOrUpdateParameter("param3", "value3"); + var expected = new Uri($"{Base}/{Resource}?param1=value1¶m2=value2¶m3=value3"); + + using var client = new RestClient($"{Base}/{Resource}?param1=value1"); + + var output = client.BuildUri(request); + Assert.Equal(expected, output); + } + + [Fact] + public void Should_build_uri_using_selected_encoding() { + var request = new RestRequest(); + // adding parameter with o-slash character which is encoded differently between + // utf-8 and iso-8859-1 + request.AddOrUpdateParameter("town", "Hillerød"); + var expectedDefaultEncoding = new Uri($"{Base}/{Resource}?town=Hiller%C3%B8d"); + var expectedIso89591Encoding = new Uri($"{Base}/{Resource}?town=Hiller%f8d"); + + using var client1 = new RestClient(new RestClientOptions($"{Base}/{Resource}")); + Assert.Equal(expectedDefaultEncoding, client1.BuildUri(request)); + + using var client2 = new RestClient(new RestClientOptions($"{Base}/{Resource}") { Encoding = Encoding.GetEncoding("ISO-8859-1") }); + Assert.Equal(expectedIso89591Encoding, client2.BuildUri(request)); + } + + [Fact] + public void Should_build_uri_with_resource_full_uri() { + var request = new RestRequest($"{Base}/connect/authorize"); + var expected = new Uri($"{Base}/connect/authorize"); + + using var client = new RestClient(Base); + + var output = client.BuildUri(request); + Assert.Equal(expected, output); + } + + [Fact] + public void Should_encode_colon() { + var request = new RestRequest(); + // adding parameter with o-slash character which is encoded differently between + // utf-8 and iso-8859-1 + request.AddOrUpdateParameter("parameter", "some:value"); + + using var client = new RestClient($"{Base}/{Resource}"); + + var expectedDefaultEncoding = new Uri($"{Base}/{Resource}?parameter=some%3avalue"); + Assert.Equal(expectedDefaultEncoding, client.BuildUri(request)); + } + + [Fact] + public void Should_not_duplicate_question_mark() { + var request = new RestRequest(); + request.AddParameter("param2", "value2"); + var expected = new Uri($"{Base}/{Resource}?param1=value1¶m2=value2"); + + using var client = new RestClient($"{Base}/{Resource}?param1=value1"); + + var output = client.BuildUri(request); + Assert.Equal(expected, output); + } + + [Fact] + public void Should_not_touch_request_url() { + const string baseUrl = "https://rs.test.org"; + const string requestUrl = "reportserver?/Prod/Report"; + + var req = new RestRequest(requestUrl, Method.Post); + + using var client = new RestClient(baseUrl); + + var resultUrl = client.BuildUri(req).ToString(); + resultUrl.Should().Be($"{baseUrl}/{requestUrl}"); + } + + [Fact] + public void Should_update_parameter_if_it_already_exists() { + var request = new RestRequest(); + request.AddOrUpdateParameter("param2", "value2"); + request.AddOrUpdateParameter("param2", "value2-1"); + var expected = new Uri($"{Base}/{Resource}?param1=value1¶m2=value2-1"); + + using var client = new RestClient($"{Base}/{Resource}?param1=value1"); + + var output = client.BuildUri(request); + Assert.Equal(expected, output); + } + + [Fact] + public void Should_use_ipv6_address() { + var baseUrl = new Uri("https://[fe80::290:e8ff:fe8b:2537%en10]:8443"); + var request = new RestRequest("api/v1/auth"); + + using var client = new RestClient(baseUrl); + + var actual = client.BuildUri(request); + actual.HostNameType.Should().Be(UriHostNameType.IPv6); + actual.AbsoluteUri.Should().Be("https://[fe80::290:e8ff:fe8b:2537]:8443/api/v1/auth"); + } + + + [Fact] + public void BuildUri_should_build_with_passing_link_as_Uri() { + // arrange + var relative = new Uri("/foo/bar/baz", UriKind.Relative); + var absoluteUri = new Uri(new(Base), relative); + var req = new RestRequest(absoluteUri); + + // act + using var client = new RestClient(); + + var builtUri = client.BuildUri(req); + + // assert + absoluteUri.Should().Be(builtUri); + } + + [Fact] + public void BuildUri_should_build_with_passing_link_as_Uri_with_set_BaseUrl() { + // arrange + var baseUrl = new Uri(Base); + var relative = new Uri("/foo/bar/baz", UriKind.Relative); + var req = new RestRequest(relative); + + // act + using var client = new RestClient(baseUrl); + + var builtUri = client.BuildUri(req); + + // assert + new Uri(baseUrl, relative).Should().Be(builtUri); + } +} \ No newline at end of file