Initial BinaryLane v2 API client

This commit is contained in:
2026-08-16 21:52:42 +10:00
commit fcd0d22b0d
91 changed files with 18691 additions and 0 deletions
+30
View File
@@ -0,0 +1,30 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
[*.{cs,csx}]
indent_size = 4
dotnet_style_qualification_for_field = false:suggestion
dotnet_style_qualification_for_property = false:suggestion
dotnet_style_qualification_for_method = false:suggestion
dotnet_style_qualification_for_event = false:suggestion
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
dotnet_style_predefined_type_for_member_access = true:suggestion
dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion
csharp_style_var_for_built_in_types = false:suggestion
csharp_style_var_when_type_is_apparent = false:suggestion
csharp_style_var_elsewhere = false:suggestion
csharp_prefer_braces = true:suggestion
csharp_style_namespace_declarations = file_scoped:suggestion
[*.{yml,yaml,json}]
indent_size = 2
[*.md]
trim_trailing_whitespace = false
+16
View File
@@ -0,0 +1,16 @@
* text=auto eol=lf
*.cs text diff=csharp
*.csproj text
*.props text
*.targets text
*.sln text
*.yml text
*.yaml text
*.json text
*.md text
*.sh text eol=lf
*.png binary
*.nupkg binary
*.snupkg binary
+12
View File
@@ -0,0 +1,12 @@
## Summary
<!-- What changes for consumers, and why? -->
## Validation
- [ ] Unit tests added or updated
- [ ] Demo builds locally
- [ ] Documentation and XML comments updated where needed
- [ ] API coverage and OpenAPI snapshot reviewed where applicable
- [ ] Changelog updated for a user-visible change
- [ ] No credentials, personal data, or sensitive infrastructure details added
+23
View File
@@ -0,0 +1,23 @@
version: 2
updates:
- package-ecosystem: nuget
directory: /
schedule:
interval: weekly
day: monday
open-pull-requests-limit: 5
groups:
dotnet:
patterns:
- "*"
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
day: monday
open-pull-requests-limit: 5
groups:
github-actions:
patterns:
- "*"
+91
View File
@@ -0,0 +1,91 @@
name: CI
on:
pull_request:
branches:
- main
push:
branches:
- main
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
DOTNET_CLI_TELEMETRY_OPTOUT: 1
DOTNET_NOLOGO: 1
NUGET_XMLDOC_MODE: skip
jobs:
build-test-pack:
name: Build, test, and pack
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Check out source
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
- name: Set up .NET
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6
with:
dotnet-version: 8.0.x
global-json-file: global.json
cache: true
cache-dependency-path: '**/packages.lock.json'
- name: Restore locked dependencies
run: |
dotnet restore src/BinaryLane.Api/BinaryLane.Api.csproj --locked-mode
dotnet restore tests/BinaryLane.Api.Tests/BinaryLane.Api.Tests.csproj --locked-mode
dotnet restore examples/BinaryLane.Api.Demo/BinaryLane.Api.Demo.csproj --locked-mode
- name: Build
run: |
dotnet build src/BinaryLane.Api/BinaryLane.Api.csproj --configuration Release --no-restore /p:ContinuousIntegrationBuild=true -p:BuildInParallel=false
dotnet build tests/BinaryLane.Api.Tests/BinaryLane.Api.Tests.csproj --configuration Release --no-restore /p:ContinuousIntegrationBuild=true -p:BuildInParallel=false
dotnet build examples/BinaryLane.Api.Demo/BinaryLane.Api.Demo.csproj --configuration Release --no-restore /p:ContinuousIntegrationBuild=true -p:BuildInParallel=false
- name: Test
run: >-
dotnet test tests/BinaryLane.Api.Tests/BinaryLane.Api.Tests.csproj --configuration Release --no-build
-m:1
--logger "trx;LogFileName=test-results.trx"
--results-directory TestResults
- name: Verify formatting
run: |
dotnet format src/BinaryLane.Api/BinaryLane.Api.csproj --verify-no-changes --no-restore
dotnet format tests/BinaryLane.Api.Tests/BinaryLane.Api.Tests.csproj --verify-no-changes --no-restore
dotnet format examples/BinaryLane.Api.Demo/BinaryLane.Api.Demo.csproj --verify-no-changes --no-restore
- name: Pack
run: >-
dotnet pack src/BinaryLane.Api/BinaryLane.Api.csproj
--configuration Release --no-build --output artifacts
-p:BuildInParallel=false
- name: Compile the demo against the packed package
run: ./eng/test-demo-package.sh artifacts
- name: Upload NuGet packages
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: nuget-packages
path: artifacts/
if-no-files-found: error
retention-days: 14
- name: Upload test results
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: test-results
path: TestResults/
if-no-files-found: ignore
retention-days: 14
+56
View File
@@ -0,0 +1,56 @@
name: CodeQL
on:
push:
branches:
- main
pull_request:
branches:
- main
schedule:
- cron: '31 4 * * 4'
workflow_dispatch:
permissions:
actions: read
contents: read
security-events: write
jobs:
analyze:
name: Analyze C#
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Check out source
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
- name: Set up .NET
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6
with:
dotnet-version: 8.0.x
global-json-file: global.json
cache: true
cache-dependency-path: '**/packages.lock.json'
- name: Initialize CodeQL
uses: github/codeql-action/init@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4
with:
languages: csharp
build-mode: manual
- name: Restore locked dependencies
run: |
dotnet restore src/BinaryLane.Api/BinaryLane.Api.csproj --locked-mode
dotnet restore tests/BinaryLane.Api.Tests/BinaryLane.Api.Tests.csproj --locked-mode
dotnet restore examples/BinaryLane.Api.Demo/BinaryLane.Api.Demo.csproj --locked-mode
- name: Build for analysis
run: |
dotnet build src/BinaryLane.Api/BinaryLane.Api.csproj --configuration Release --no-restore -p:BuildInParallel=false
dotnet build tests/BinaryLane.Api.Tests/BinaryLane.Api.Tests.csproj --configuration Release --no-restore -p:BuildInParallel=false
dotnet build examples/BinaryLane.Api.Demo/BinaryLane.Api.Demo.csproj --configuration Release --no-restore -p:BuildInParallel=false
- name: Perform CodeQL analysis
uses: github/codeql-action/analyze@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4
+92
View File
@@ -0,0 +1,92 @@
name: Monitor upstream OpenAPI contract
on:
schedule:
- cron: '17 3 * * 1'
workflow_dispatch:
permissions:
contents: read
issues: write
concurrency:
group: contract-monitor
cancel-in-progress: false
jobs:
check:
name: Check BinaryLane OpenAPI contract
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Check out source
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
- name: Compare live contract with the reviewed snapshot
id: contract
shell: bash
run: |
set +e
./eng/verify-openapi-contract.sh > contract-check.txt 2>&1
status=$?
set -e
cat contract-check.txt
case "$status" in
0)
echo "changed=false" >> "$GITHUB_OUTPUT"
;;
1)
echo "changed=true" >> "$GITHUB_OUTPUT"
;;
*)
echo "The contract check could not complete (exit $status)." >&2
exit "$status"
;;
esac
- name: Upload contract-check report
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: contract-check
path: contract-check.txt
if-no-files-found: ignore
retention-days: 14
- name: Open one review issue for contract drift
if: steps.contract.outputs.changed == 'true'
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
existing_count="$(gh issue list \
--repo "$GITHUB_REPOSITORY" \
--state open \
--search 'in:title "BinaryLane OpenAPI contract changed"' \
--json number \
--jq 'length')"
if [[ "$existing_count" -gt 0 ]]; then
echo "An upstream-contract review issue is already open."
exit 0
fi
{
echo "BinaryLane's public OpenAPI contract no longer matches the reviewed snapshot."
echo
echo "Review the upstream change before updating generated code or SDK behavior."
echo
echo "```text"
cat contract-check.txt
echo "```"
echo
echo "Suggested next step: run `./eng/refresh-openapi-contract.sh`, review the diff,"
echo "then update models, fixtures, API coverage, docs, and tests in one pull request."
} > issue-body.md
gh issue create \
--repo "$GITHUB_REPOSITORY" \
--title "BinaryLane OpenAPI contract changed" \
--body-file issue-body.md
+186
View File
@@ -0,0 +1,186 @@
name: Publish
on:
push:
tags:
- 'v*'
permissions:
contents: read
concurrency:
group: publish-${{ github.ref }}
cancel-in-progress: false
env:
DOTNET_CLI_TELEMETRY_OPTOUT: 1
DOTNET_NOLOGO: 1
NUGET_XMLDOC_MODE: skip
jobs:
build:
name: Validate, test, and pack
if: github.repository == 'alexhopeoconnor/binarylane-dotnet'
runs-on: ubuntu-latest
timeout-minutes: 20
outputs:
package_version: ${{ steps.package_version.outputs.value }}
steps:
- name: Check out tagged source
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
fetch-depth: 0
- name: Set up .NET
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6
with:
dotnet-version: 8.0.x
global-json-file: global.json
cache: true
cache-dependency-path: '**/packages.lock.json'
- name: Verify the tagged commit is reachable from main
run: |
git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then
echo "Release tags must point to a commit reachable from main." >&2
exit 1
fi
- name: Assert tag, package version, and changelog agree
run: ./eng/assert-release-version.sh "${{ github.ref_name }}"
- name: Read package version
id: package_version
run: echo "value=$(./eng/read-package-version.sh)" >> "$GITHUB_OUTPUT"
- name: Restore locked dependencies
run: |
dotnet restore src/BinaryLane.Api/BinaryLane.Api.csproj --locked-mode
dotnet restore tests/BinaryLane.Api.Tests/BinaryLane.Api.Tests.csproj --locked-mode
dotnet restore examples/BinaryLane.Api.Demo/BinaryLane.Api.Demo.csproj --locked-mode
- name: Build
run: |
dotnet build src/BinaryLane.Api/BinaryLane.Api.csproj --configuration Release --no-restore /p:ContinuousIntegrationBuild=true -p:BuildInParallel=false
dotnet build tests/BinaryLane.Api.Tests/BinaryLane.Api.Tests.csproj --configuration Release --no-restore /p:ContinuousIntegrationBuild=true -p:BuildInParallel=false
dotnet build examples/BinaryLane.Api.Demo/BinaryLane.Api.Demo.csproj --configuration Release --no-restore /p:ContinuousIntegrationBuild=true -p:BuildInParallel=false
- name: Test
run: dotnet test tests/BinaryLane.Api.Tests/BinaryLane.Api.Tests.csproj --configuration Release --no-build -m:1
- name: Pack
run: >-
dotnet pack src/BinaryLane.Api/BinaryLane.Api.csproj
--configuration Release --no-build --output artifacts
-p:BuildInParallel=false
- name: Compile the demo against the packed package
run: ./eng/test-demo-package.sh artifacts
- name: Upload NuGet packages
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: nuget-packages
path: artifacts/
if-no-files-found: error
retention-days: 14
publish:
name: Publish to NuGet.org
needs: build
if: github.repository == 'alexhopeoconnor/binarylane-dotnet'
runs-on: ubuntu-latest
timeout-minutes: 10
environment: release
permissions:
contents: read
id-token: write
steps:
- name: Download NuGet packages
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: nuget-packages
path: artifacts
- name: Check trusted-publishing configuration
run: |
if [[ -z "${{ secrets.NUGET_USER }}" ]]; then
echo "Set the protected release-environment secret NUGET_USER to the NuGet.org username." >&2
exit 1
fi
- name: Authenticate to NuGet.org through GitHub OIDC
id: nuget_login
uses: NuGet/login@8d196754b4036150537f80ac539e15c2f1028841 # v1
with:
user: ${{ secrets.NUGET_USER }}
- name: Publish package
env:
NUGET_API_KEY: ${{ steps.nuget_login.outputs.NUGET_API_KEY }}
run: >-
dotnet nuget push "artifacts/*.nupkg"
--source https://api.nuget.org/v3/index.json
--api-key "$NUGET_API_KEY"
--skip-duplicate
- name: Publish symbols
env:
NUGET_API_KEY: ${{ steps.nuget_login.outputs.NUGET_API_KEY }}
shell: bash
run: |
shopt -s nullglob
for package in artifacts/*.snupkg; do
dotnet nuget push "$package" \
--source https://api.nuget.org/v3/index.json \
--api-key "$NUGET_API_KEY" \
--skip-duplicate
done
release:
name: Create GitHub Release
needs:
- build
- publish
if: github.repository == 'alexhopeoconnor/binarylane-dotnet'
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
steps:
- name: Check out tagged source
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
- name: Download NuGet packages
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: nuget-packages
path: artifacts
- name: Extract release notes
run: ./eng/extract-release-notes.sh "${{ needs.build.outputs.package_version }}" > release-notes.md
- name: Create or update GitHub Release
env:
GH_TOKEN: ${{ github.token }}
PACKAGE_VERSION: ${{ needs.build.outputs.package_version }}
TAG_NAME: ${{ github.ref_name }}
shell: bash
run: |
if gh release view "$TAG_NAME" --repo "$GITHUB_REPOSITORY" > /dev/null 2>&1; then
gh release upload "$TAG_NAME" artifacts/* eng/openapi/contract.json --clobber --repo "$GITHUB_REPOSITORY"
gh release edit "$TAG_NAME" \
--title "BinaryLane.Api $PACKAGE_VERSION" \
--notes-file release-notes.md \
--repo "$GITHUB_REPOSITORY"
else
gh release create "$TAG_NAME" artifacts/* eng/openapi/contract.json \
--title "BinaryLane.Api $PACKAGE_VERSION" \
--notes-file release-notes.md \
--verify-tag \
--repo "$GITHUB_REPOSITORY"
fi
+31
View File
@@ -0,0 +1,31 @@
# Build output
[Bb]in/
[Oo]bj/
[Aa]rtifacts/
[Tt]est[Rr]esult*/
coverage/
*.coverage
*.coveragexml
# IDE and user-specific files
.vs/
.vscode/
*.user
*.suo
*.userosscache
*.sln.docstates
# Local configuration and credentials
.env
.env.*
!.env.example
appsettings.Development.json
secrets.json
# Operating-system files
.DS_Store
Thumbs.db
# Contract-monitor working files
eng/openapi/*.downloaded.yaml
eng/openapi/*.normalized.yaml
+28
View File
@@ -0,0 +1,28 @@
# Changelog
All notable changes to this project are documented here. This project follows
[Semantic Versioning](https://semver.org/). The SDK version describes the
public .NET API; it is deliberately independent from BinaryLane's preview API
version.
## [Unreleased]
### Added
- Nothing yet.
## [0.1.0-beta.1] - 2026-08-16
### Added
- Initial preview release of the unofficial community .NET client for the
BinaryLane v2 API.
- Typed HTTP client setup, pluggable bearer-token authentication, pagination,
API error mapping, and a maintained read-only demo application.
- HTTPS-only bearer-token transport, protected request authority and
authorization headers, and a 16 MiB successful-response buffer limit.
- OpenAPI contract snapshot and change-monitoring automation for BinaryLane
API reference version `0.39.1`.
[Unreleased]: https://github.com/alexhopeoconnor/binarylane-dotnet/compare/v0.1.0-beta.1...HEAD
[0.1.0-beta.1]: https://github.com/alexhopeoconnor/binarylane-dotnet/releases/tag/v0.1.0-beta.1
+71
View File
@@ -0,0 +1,71 @@
# Contributing
Thanks for improving the SDK. Keep changes focused, tested, documented, and
backwards compatible unless a breaking change is explicitly planned.
## Local setup
Install the SDK pinned in `global.json`, then run:
```bash
dotnet restore src/BinaryLane.Api/BinaryLane.Api.csproj --locked-mode
dotnet restore tests/BinaryLane.Api.Tests/BinaryLane.Api.Tests.csproj --locked-mode
dotnet restore examples/BinaryLane.Api.Demo/BinaryLane.Api.Demo.csproj --locked-mode
dotnet build src/BinaryLane.Api/BinaryLane.Api.csproj -c Release --no-restore
dotnet build tests/BinaryLane.Api.Tests/BinaryLane.Api.Tests.csproj -c Release --no-restore
dotnet build examples/BinaryLane.Api.Demo/BinaryLane.Api.Demo.csproj -c Release --no-restore
dotnet test tests/BinaryLane.Api.Tests/BinaryLane.Api.Tests.csproj -c Release --no-build
dotnet format src/BinaryLane.Api/BinaryLane.Api.csproj --verify-no-changes --no-restore
dotnet format tests/BinaryLane.Api.Tests/BinaryLane.Api.Tests.csproj --verify-no-changes --no-restore
dotnet format examples/BinaryLane.Api.Demo/BinaryLane.Api.Demo.csproj --verify-no-changes --no-restore
```
The tracked demo application lives in
`examples/BinaryLane.Api.Demo`. Configure a personal API token with user
secrets; never put it in a file, test fixture, issue, pull request, or commit.
```bash
dotnet user-secrets set \
"BinaryLane:ApiToken" "your-token" \
--project examples/BinaryLane.Api.Demo
```
## Pull requests
- Keep each pull request narrowly scoped and explain the user-visible effect.
- Add or update unit tests for every changed HTTP request, response, error, or
public API contract.
- Use scrubbed fixtures only. Never record a real bearer token, password,
user-data payload, IP address, server name, or account email unless it is
demonstrably non-sensitive and intended for publication.
- Update XML documentation, the README, and the appropriate page in `docs/`
when a public behavior changes.
- Update `docs/api-coverage.md` when support for a documented BinaryLane route
changes.
- Add a changelog entry under **Unreleased** for a user-visible change.
## API design rules
- Keep public APIs in `BinaryLane.Api.V2.*`.
- Prefer focused resource interfaces over a monolithic service interface.
- Accept a `CancellationToken` on every asynchronous operation.
- Keep provider fields forward compatible: do not turn preview response values
into closed C# enums unless the API contract guarantees them.
- Do not add automatic retries for state-changing requests. BinaryLane does not
document idempotency keys, so retries can provision or mutate resources twice.
- Avoid breaking changes in a minor or patch SDK version. Follow
[Semantic Versioning](https://semver.org/).
## Upstream OpenAPI changes
The provider's OpenAPI document is a preview contract and can change without a
version increment. See [the contract workflow](docs/upstream-contract.md)
before updating `eng/openapi/`. Review any change by hand; the raw contract
contains virtual `#ActionName` documentation paths that must not become real
HTTP endpoints in the SDK.
## Releases
Only maintainers may release packages. See [the release guide](docs/releasing.md).
The release workflow uses NuGet trusted publishing through GitHub OIDC; no
long-lived NuGet API key belongs in this repository or its GitHub secrets.
+13
View File
@@ -0,0 +1,13 @@
<Project>
<PropertyGroup>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>latest</LangVersion>
<AnalysisLevel>latest-recommended</AnalysisLevel>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Deterministic>true</Deterministic>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<RestoreLockedMode Condition="'$(CI)' == 'true'">true</RestoreLockedMode>
<ContinuousIntegrationBuild Condition="'$(CI)' == 'true'">true</ContinuousIntegrationBuild>
</PropertyGroup>
</Project>
+25
View File
@@ -0,0 +1,25 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Configuration.UserSecrets" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="8.0.2" />
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="8.0.0" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="8.0.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
<PackageVersion Include="System.Text.Json" Version="8.0.5" />
<PackageVersion Include="xunit" Version="2.9.2" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup>
</Project>
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Alex Hope-O'Connor
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.
+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<!-- Keep restores reproducible and prevent inherited workstation feeds from
becoming accidental package dependencies. -->
<packageSources>
<clear />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
</packageSources>
</configuration>
+94
View File
@@ -0,0 +1,94 @@
# BinaryLane.Api
[![CI](https://github.com/alexhopeoconnor/binarylane-dotnet/actions/workflows/ci.yml/badge.svg)](https://github.com/alexhopeoconnor/binarylane-dotnet/actions/workflows/ci.yml)
[![NuGet](https://img.shields.io/nuget/vpre/BinaryLane.Api.svg)](https://www.nuget.org/packages/BinaryLane.Api)
`BinaryLane.Api` is an unofficial .NET client for the BinaryLane v2 API. It
provides typed clients for account, billing, servers, images, DNS, load
balancers, VPCs, and other v2 resources.
See the [BinaryLane API reference](https://api.binarylane.com.au/reference/) for
provider-specific API behaviour.
BinaryLane describes its API as a developer preview. Pin the package version
used by production applications and review release notes before upgrading.
> This project is not affiliated with, endorsed by, or supported by BinaryLane.
> For account or infrastructure support, use
> [BinaryLane Support](https://support.binarylane.com.au/support/home).
## Install
```bash
dotnet add package BinaryLane.Api --prerelease
```
Remove `--prerelease` once a stable package version is available.
## Quick start
Keep the API token in a secret store, user secrets, or an environment variable.
Do not commit it to configuration or source control.
```csharp
using BinaryLane.Api.V2;
using BinaryLane.Api.V2.DependencyInjection;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddBinaryLaneApi(options =>
{
options.ApiToken = builder.Configuration["BinaryLane:ApiToken"]
?? Environment.GetEnvironmentVariable("BINARYLANE_API_TOKEN")
?? throw new InvalidOperationException("A BinaryLane API token is required.");
});
var app = builder.Build();
app.MapGet("/account", async (IBinaryLaneClient binaryLane, CancellationToken cancellationToken) =>
await binaryLane.Account.GetAsync(cancellationToken));
app.Run();
```
Use the resource clients from `IBinaryLaneClient`:
```csharp
static async Task ListServersAsync(
IBinaryLaneClient binaryLane,
CancellationToken cancellationToken)
{
await foreach (var server in binaryLane.Servers.ListAllAsync(cancellationToken: cancellationToken))
{
Console.WriteLine($"{server.Id}: {server.Name}");
}
}
```
List methods support `CancellationToken`. Mutating calls are sent once; add a
retry policy only when the operation is known to be safe to repeat.
## Documentation
| Guide | Use it for |
| --- | --- |
| [Getting started](https://github.com/alexhopeoconnor/binarylane-dotnet/blob/main/docs/getting-started.md) | Your first request and user-secrets setup. |
| [Configuration](https://github.com/alexhopeoconnor/binarylane-dotnet/blob/main/docs/configuration.md) | Tokens, timeouts, direct construction, and HTTP configuration. |
| [Pagination](https://github.com/alexhopeoconnor/binarylane-dotnet/blob/main/docs/pagination.md) | Working with pages or async enumeration. |
| [Actions](https://github.com/alexhopeoconnor/binarylane-dotnet/blob/main/docs/actions-and-polling.md) | Submitting and optionally waiting for server actions. |
| [Error handling](https://github.com/alexhopeoconnor/binarylane-dotnet/blob/main/docs/errors.md) | Handling API failures safely. |
| [Demo](https://github.com/alexhopeoconnor/binarylane-dotnet/tree/main/examples/BinaryLane.Api.Demo) | Running the included read-only console app. |
## Compatibility
The package targets `net8.0` and `netstandard2.0`; .NET 10 applications use
the `net8.0` asset. Package versions follow [Semantic Versioning](https://semver.org/).
## Contributing and security
See [CONTRIBUTING.md](https://github.com/alexhopeoconnor/binarylane-dotnet/blob/main/CONTRIBUTING.md) for development guidance and
[SECURITY.md](https://github.com/alexhopeoconnor/binarylane-dotnet/blob/main/SECURITY.md) for private vulnerability reporting.
## License
Distributed under the [MIT License](https://github.com/alexhopeoconnor/binarylane-dotnet/blob/main/LICENSE).
+25
View File
@@ -0,0 +1,25 @@
# Security policy
## Supported versions
Security fixes are made for the latest released package version. While the SDK
is pre-1.0, upgrade to the latest beta before reporting a suspected issue.
## Reporting a vulnerability
Please report vulnerabilities privately through
[GitHub Security Advisories](https://github.com/alexhopeoconnor/binarylane-dotnet/security/advisories/new).
If private reporting is unavailable, open a minimal public issue that asks for
a private contact channel and does not disclose exploit details.
Do not include any of the following in a report, issue, pull request, log, or
sample:
- BinaryLane bearer tokens or authorization headers;
- account email addresses, invoices, or billing data;
- passwords, password reset payloads, SSH private keys, or `user_data`;
- private IP addresses, server names, DNS records, or raw response bodies that
are not essential to demonstrate the problem.
Maintainers aim to acknowledge a report within seven days and will coordinate
a fix and disclosure timeline with the reporter.
+10
View File
@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256" role="img" aria-labelledby="title">
<title id="title">Abstract network route icon</title>
<rect width="256" height="256" rx="52" fill="#172554"/>
<path d="M66 82h55c19 0 35 16 35 35v57" fill="none" stroke="#67E8F9" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/>
<path d="M100 174h35c19 0 35-16 35-35V82" fill="none" stroke="#A7F3D0" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/>
<circle cx="66" cy="82" r="17" fill="#F8FAFC"/>
<circle cx="100" cy="174" r="17" fill="#F8FAFC"/>
<circle cx="156" cy="174" r="17" fill="#F8FAFC"/>
<circle cx="170" cy="82" r="17" fill="#F8FAFC"/>
</svg>

After

Width:  |  Height:  |  Size: 727 B

+48
View File
@@ -0,0 +1,48 @@
# Server actions
Use a typed `ServerAction` to submit a server action. For example, this powers
on a server:
```csharp
using BinaryLane.Api.V2.Models;
var submission = await client.Servers.SubmitActionAsync(
serverId,
new PowerOnServerAction(),
cancellationToken);
```
Common action types include `PowerOnServerAction`, `PowerOffServerAction`,
`RebootServerAction`, `ResizeServerAction`, and `RebuildServerAction`.
## Waiting for completion
BinaryLane can accept an action before it has finished. When the response
contains an action, use its identifier to wait for the final status:
```csharp
if (submission.Action is { } action)
{
var completed = await client.Actions.WaitForCompletionAsync(
action.Id,
cancellationToken: cancellationToken);
}
```
Pass `ActionWaitOptions` when you need a different timeout or polling interval.
The default is a 15-minute timeout with a two-second polling interval.
## Status values
`BinaryLaneAction.Status` and `BinaryLaneAction.Type` are strings. Compare a
documented value with `BinaryLaneValues`:
```csharp
if (action.Status == BinaryLaneValues.ActionStatus.Completed)
{
// Continue with the next step.
}
```
If an action request has an uncertain outcome, retrieve the action or server
state before submitting it again.
+33
View File
@@ -0,0 +1,33 @@
# Maintainer guide: API coverage
This is a maintainer reference. Package consumers can start with the
[README](../README.md).
The SDK's scope is the BinaryLane v2 OpenAPI contract committed at
`eng/openapi/binarylane-v2.openapi.yaml`. The raw contract currently declares
version `0.39.1`; it is a developer-preview contract and may change without a
version change.
The raw document contains virtual paths such as
`/v2/servers/{server_id}/actions#PowerOn`. These are API-reference aliases for
action payload variants, not real HTTP routes. Coverage and generated-code
checks must use the normalized contract and count only the real
`POST /v2/servers/{server_id}/actions` route.
| API area | Public resource boundary |
| --- | --- |
| Account | `IAccountApi` |
| Balances, invoices, and unpaid invoices | `IBillingApi` |
| Actions | `IActionsApi` |
| Servers and server subresources | `IServersApi` |
| Images | `IImagesApi` |
| SSH keys | `ISshKeysApi` |
| DNS domains, nameservers, and records | `IDomainsApi` |
| Load balancers and forwarding rules | `ILoadBalancersApi` |
| VPCs and members | `IVpcsApi` |
| Regions, sizes, and software catalogues | `IRegionsApi`, `ISizesApi`, `ISoftwareApi` |
| Reverse names | `IReverseNamesApi` |
| Data usage | `IDataUsageApi` |
| Sample sets | `ISampleSetsApi` |
Keep this table and the related tests up to date when API support changes.
+72
View File
@@ -0,0 +1,72 @@
# Configuration
## Standard registration
In an ASP.NET Core or generic-host application, `AddBinaryLaneApi` configures
`IBinaryLaneClient` and the individual resource interfaces.
```csharp
builder.Services.AddBinaryLaneApi(options =>
{
options.ApiToken = builder.Configuration["BinaryLane:ApiToken"];
options.RequestTimeoutSeconds = 100;
});
```
| Setting | Default | Description |
| --- | --- | --- |
| `BaseUrl` | `https://api.binarylane.com.au/` | API base address. Leave unchanged for BinaryLane's public API. |
| `ApiToken` | none | Bearer token used by the default provider. |
| `RequestTimeoutSeconds` | `100` | Timeout for an individual HTTP request. |
The base URL must be an HTTPS URL without credentials, a query string, or a
fragment. Options are validated when the application starts.
## Rotating tokens
Register `IBinaryLaneTokenProvider` before calling `AddBinaryLaneApi` when the
token comes from a vault or changes during the lifetime of the application.
`VaultTokenProvider` below is your implementation of that interface.
```csharp
using BinaryLane.Api.V2.Authentication;
using BinaryLane.Api.V2.DependencyInjection;
builder.Services.AddSingleton<IBinaryLaneTokenProvider, VaultTokenProvider>();
builder.Services.AddBinaryLaneApi(options => options.RequestTimeoutSeconds = 100);
```
The provider is called for every request. Do not log the returned token.
## Use without dependency injection
```csharp
using BinaryLane.Api.V2;
using BinaryLane.Api.V2.Authentication;
using var httpClient = new HttpClient
{
BaseAddress = new Uri("https://api.binarylane.com.au/"),
};
var token = Environment.GetEnvironmentVariable("BINARYLANE_API_TOKEN")
?? throw new InvalidOperationException("Set BINARYLANE_API_TOKEN.");
var client = new BinaryLaneClient(
httpClient,
new StaticBinaryLaneTokenProvider(token));
```
## HTTP configuration
`AddBinaryLaneApi` returns an `IHttpClientBuilder`, so you can configure proxy,
telemetry, or other application-specific handlers.
```csharp
builder.Services.AddBinaryLaneApi(options => options.ApiToken = token)
.ConfigureHttpClient(client => client.DefaultRequestHeaders.Add("X-App", "my-service"));
```
Only add automatic retries when the request is safe to repeat. In particular,
POST, PUT, PATCH, and DELETE requests may have already been applied when a
network failure is reported.
+40
View File
@@ -0,0 +1,40 @@
# Error handling
All API failures derive from `BinaryLaneApiException`.
| Exception | When it is used |
| --- | --- |
| `BinaryLaneUnauthorizedException` | The token is missing, invalid, or expired. |
| `BinaryLaneForbiddenException` | The token does not have access to the resource. |
| `BinaryLaneNotFoundException` | The requested resource does not exist or is not visible to the token. |
| `BinaryLaneValidationException` | BinaryLane rejected the request payload. |
| `BinaryLaneApiException` | Any other non-success HTTP response. |
```csharp
using BinaryLane.Api.V2.Errors;
try
{
var server = await client.Servers.GetAsync(serverId, cancellationToken);
}
catch (BinaryLaneNotFoundException)
{
// Handle a missing server.
}
catch (BinaryLaneValidationException)
{
// Show an appropriate validation message to the caller.
}
catch (BinaryLaneApiException exception)
{
// Use exception.StatusCode to choose an application-specific response.
}
```
Each exception provides `StatusCode`, `RequestUri`, `Headers`, and, when
available, `Problem` and `ResponseBody`. Provider detail text can contain user
data. Do not write `Problem`, `Headers`, or `ResponseBody` to application logs
without appropriate redaction.
Successful JSON responses are limited to 16 MiB. A larger response throws
`HttpRequestException` before the client buffers it in memory.
+53
View File
@@ -0,0 +1,53 @@
# Getting started
`BinaryLane.Api` connects to BinaryLane's bearer-token v2 API at
`https://api.binarylane.com.au/v2/`.
## 1. Install the package
```bash
dotnet add package BinaryLane.Api --prerelease
```
## 2. Store the token safely
For local development, initialise user secrets for your application and add
the token:
```bash
dotnet user-secrets init
dotnet user-secrets set "BinaryLane:ApiToken" "your-token"
```
For hosted applications, use the platform's secret store or supply
`BINARYLANE_API_TOKEN` at runtime.
## 3. Register the client
```csharp
using BinaryLane.Api.V2;
using BinaryLane.Api.V2.DependencyInjection;
using BinaryLane.Api.V2.Models;
builder.Services.AddBinaryLaneApi(options =>
{
options.ApiToken = builder.Configuration["BinaryLane:ApiToken"]
?? Environment.GetEnvironmentVariable("BINARYLANE_API_TOKEN")
?? throw new InvalidOperationException("A BinaryLane API token is required.");
});
```
## 4. Make a request
Inject `IBinaryLaneClient`, then select the resource you need:
```csharp
public sealed class ServerReader(IBinaryLaneClient binaryLane)
{
public IAsyncEnumerable<Server> ListAsync(CancellationToken cancellationToken) =>
binaryLane.Servers.ListAllAsync(cancellationToken: cancellationToken);
}
```
See [Configuration](configuration.md) for custom token providers and
[Pagination](pagination.md) for page-by-page access.
+31
View File
@@ -0,0 +1,31 @@
# Pagination
List endpoints accept one-based `page` and `per_page` values. `per_page` can be
between 1 and 200.
Use `ListAsync` when you need a specific page or the provider's page links:
```csharp
using BinaryLane.Api.V2.Pagination;
var page = await client.Servers.ListAsync(
new PageRequest { Page = 1, PerPage = 50 },
cancellationToken);
foreach (var server in page.Items)
{
await ProcessAsync(server, cancellationToken);
}
```
Use `ListAllAsync` to process every page as it is needed:
```csharp
await foreach (var server in client.Servers.ListAllAsync(cancellationToken: cancellationToken))
{
await ProcessAsync(server, cancellationToken);
}
```
Pass a cancellation token from the calling request, worker, or command so a
stopped operation does not continue fetching pages.
+63
View File
@@ -0,0 +1,63 @@
# Maintainer guide: releases
This page is for package maintainers.
This repository publishes packages with NuGet trusted publishing and GitHub
OpenID Connect (OIDC). It must not use a long-lived NuGet API key.
## One-time setup
1. On NuGet.org, create or verify the trusted-publishing policy:
- publisher: `GitHubActions`;
- GitHub owner: `alexhopeoconnor`;
- repository: `binarylane-dotnet`;
- workflow: `publish.yml`;
- environment: `release`.
2. In GitHub, create a protected environment named `release` and require a
maintainer approval.
3. Create a protected `release` environment secret named `NUGET_USER` with
the NuGet.org username `alex.hope.oconnor`. It is an identifier, not an
API key.
4. Protect `main` with CI and restrict creation of `v*` tags.
The first successful OIDC publication must occur before NuGet's policy
activation window expires. After success, NuGet permanently activates the
policy for this exact repository/workflow/environment identity.
## Release checklist
1. Review BinaryLane contract changes and update the committed snapshot if
necessary.
2. Update code, tests, docs, API coverage, and `CHANGELOG.md`.
3. Set the package `<Version>` to the intended SemVer release.
4. Open and merge the release pull request after CI succeeds.
5. Tag the exact merge commit. The tag must equal the package version with a
leading `v`.
```bash
git tag -a v0.1.0-beta.1 -m "BinaryLane.Api 0.1.0-beta.1"
git push origin v0.1.0-beta.1
```
6. Approve the protected `release` environment in GitHub Actions.
7. Verify the package page, rendered README, icon, license, repository link,
symbols, package ownership, and GitHub Release.
## What the release workflow does
`publish.yml` validates the tag, package version, and changelog heading; it
then restores, builds, tests, packs, and compiles the maintained demo against
the resulting local `.nupkg`. Only then does `NuGet/login@v1` exchange the
GitHub OIDC identity for a short-lived one-time publish key. The key is used
only in memory by the job.
The GitHub Release job runs after NuGet publishing and receives no OIDC
permission. It attaches the package files and uses the matching changelog
section for release notes.
## Recovery
NuGet packages cannot be overwritten. If a release has a packaging defect,
unlist it if appropriate, publish a new version, and document the correction
in the changelog. Do not retag a version or attempt to reuse a published
package version.
+48
View File
@@ -0,0 +1,48 @@
# Maintainer guide: upstream API contract
This page describes SDK maintenance. Package consumers can start with the
[README](../README.md).
The source of truth for BinaryLane's API is its published OpenAPI document:
<https://api.binarylane.com.au/reference/openapi.yaml>
The committed raw snapshot is intentionally reviewable:
```text
eng/openapi/binarylane-v2.openapi.yaml
eng/openapi/contract.json
```
`contract.json` records the source URL, provider-declared version, SHA-256,
and retrieval time. It describes an upstream artifact; it does not set the
NuGet package version.
## Snapshot purpose
BinaryLane's preview API can change without a version change. The committed
snapshot makes those changes visible during SDK maintenance.
## Normalization
The raw document includes virtual `#ActionName` paths for individual server
action variants. They exist to improve the provider's reference UI but are not
HTTP paths. `eng/normalize-openapi.sh` strips them when producing a normalized
document for code generation or coverage checks. Do not issue requests to
those virtual paths.
## Monitoring and updating
The scheduled `contract-monitor.yml` workflow downloads the live document and
runs `eng/verify-openapi-contract.sh`. If the SHA-256 or provider-declared
version differs, it opens one issue rather than silently updating the SDK.
To intentionally refresh the snapshot after review:
```bash
./eng/refresh-openapi-contract.sh
git diff -- eng/openapi docs/api-coverage.md
```
Then update affected models, coverage documentation, changelog, and tests in
the same pull request. Review the change before merging it.
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env bash
set -euo pipefail
# Enforces the release identity before any package is published. The version is
# intentionally read from the package project so one source of truth controls
# both the .nupkg and its corresponding Git tag.
if [[ $# -ne 1 ]]; then
printf 'Usage: %s v<package-version>\n' "${0##*/}" >&2
exit 2
fi
tag_name="$1"
script_dir="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
repository_root="$(CDPATH= cd -- "$script_dir/.." && pwd)"
changelog_file="$repository_root/CHANGELOG.md"
if [[ ! -f "$changelog_file" ]]; then
printf 'Expected changelog does not exist.\n' >&2
exit 2
fi
package_version="$("$script_dir/read-package-version.sh")"
expected_tag="v${package_version}"
if [[ "$tag_name" != "$expected_tag" ]]; then
printf 'Tag %s does not match package version %s (expected %s).\n' \
"$tag_name" "$package_version" "$expected_tag" >&2
exit 1
fi
if ! grep -Fqx "## [$package_version]" "$changelog_file" && \
! grep -Fq "## [$package_version] -" "$changelog_file" && \
! grep -Fqx "## $package_version" "$changelog_file" && \
! grep -Fq "## $package_version -" "$changelog_file"; then
printf 'CHANGELOG.md needs a level-two heading for %s.\n' "$package_version" >&2
exit 1
fi
printf 'Release version is consistent: %s.\n' "$package_version"
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -ne 1 ]]; then
printf 'Usage: %s <package-version>\n' "${0##*/}" >&2
exit 2
fi
package_version="$1"
script_dir="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
repository_root="$(CDPATH= cd -- "$script_dir/.." && pwd)"
changelog_file="$repository_root/CHANGELOG.md"
notes="$(awk -v bracket_heading="## [$package_version]" -v bare_heading="## $package_version" '
$0 == bracket_heading || index($0, bracket_heading " -") == 1 ||
$0 == bare_heading || index($0, bare_heading " -") == 1 {
capture = 1
next
}
capture && /^## / {
exit
}
capture && /^\[/ {
exit
}
capture {
print
}
' "$changelog_file")"
if [[ -z "${notes//[[:space:]]/}" ]]; then
printf 'No release notes found for %s in CHANGELOG.md.\n' "$package_version" >&2
exit 1
fi
printf '%s\n' "$notes"
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
set -euo pipefail
# Normalize BinaryLane's raw OpenAPI document for code-generation or coverage
# tooling. The reference includes virtual `#ActionName` paths for individual
# server action payloads. They are documentation aliases, not HTTP endpoints.
script_dir="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
default_input="$script_dir/openapi/binarylane-v2.openapi.yaml"
input_file="${1:-$default_input}"
output_file="${2:-}"
if [[ ! -f "$input_file" ]]; then
printf 'OpenAPI input does not exist: %s\n' "$input_file" >&2
exit 2
fi
normalize() {
awk '
/^paths:$/ {
in_paths = 1
}
in_paths && /^components:$/ {
in_paths = 0
skip_virtual_path = 0
}
in_paths && /^ ['"'"'][^'"'"']*#[^'"'"']*['"'"']:[[:space:]]*$/ {
skip_virtual_path = 1
removed += 1
next
}
in_paths && /^ [^[:space:]][^:]*:[[:space:]]*$/ {
skip_virtual_path = 0
}
!skip_virtual_path {
print
}
END {
printf "Removed %d virtual action path(s).\n", removed > "/dev/stderr"
}
' "$input_file"
}
if [[ -n "$output_file" ]]; then
mkdir -p "$(dirname -- "$output_file")"
normalize > "$output_file"
else
normalize
fi
+21
View File
@@ -0,0 +1,21 @@
# BinaryLane API contract
`binarylane-v2.openapi.yaml` is an unmodified snapshot of BinaryLane's published
OpenAPI document. Its provenance and digest are recorded in `contract.json`.
The provider currently describes the API as a developer preview, so an upstream
version number alone must not be used as a compatibility guarantee. The contract
monitor compares the document digest as well as its declared version.
## Normalization rule
The document has 52 routable paths and 42 additional paths such as
`/v2/servers/{server_id}/actions#PowerOn`. The latter are documentation aliases
for payload variants of `POST /v2/servers/{server_id}/actions`; URI fragments do
not reach an HTTP server. They must be removed before an OpenAPI generator is
asked to create a route surface. `normalization.json` records the exact rule and
each affected path.
Public SDK DTOs deliberately model all provider enum values as strings. This is
forward-compatible with a preview API that may add a value before a new SDK
release is available.
File diff suppressed because it is too large Load Diff
+6
View File
@@ -0,0 +1,6 @@
{
"source": "https://api.binarylane.com.au/reference/openapi.yaml",
"upstreamVersion": "0.39.1",
"sha256": "947b62c1649f68e8b0688d113f4fa3b5037b7c514b7e9cd33613e40574f854b1",
"retrievedUtc": "2026-08-16T09:22:01Z"
}
+64
View File
@@ -0,0 +1,64 @@
{
"input": "binarylane-v2.openapi.yaml",
"normalizationRules": [
{
"kind": "exclude-paths-containing",
"value": "#",
"reason": "The upstream document uses fragment-bearing paths to document variants of the one real server actions endpoint. A URI fragment is never sent in an HTTP request."
},
{
"kind": "preserve-discriminator",
"schema": "ServerAction",
"property": "type",
"reason": "The action body is a discriminated union. Do not flatten or silently discard the discriminator mapping when generating test fixtures or internal wire code."
},
{
"kind": "open-enum",
"reason": "Provider enum values are represented as strings in public DTOs so new upstream values remain readable by existing SDK versions."
}
],
"excludedPaths": [
"/v2/servers/{server_id}/actions#AddDisk",
"/v2/servers/{server_id}/actions#AttachBackup",
"/v2/servers/{server_id}/actions#ChangeAdvancedFeatures",
"/v2/servers/{server_id}/actions#ChangeAdvancedFirewallRules",
"/v2/servers/{server_id}/actions#ChangeBackupSchedule",
"/v2/servers/{server_id}/actions#ChangeIpv6",
"/v2/servers/{server_id}/actions#ChangeIpv6ReverseNameservers",
"/v2/servers/{server_id}/actions#ChangeKernel",
"/v2/servers/{server_id}/actions#ChangeManageOffsiteBackupCopies",
"/v2/servers/{server_id}/actions#ChangeNetwork",
"/v2/servers/{server_id}/actions#ChangeOffsiteBackupLocation",
"/v2/servers/{server_id}/actions#ChangePartner",
"/v2/servers/{server_id}/actions#ChangePortBlocking",
"/v2/servers/{server_id}/actions#ChangeRegion",
"/v2/servers/{server_id}/actions#ChangeReverseName",
"/v2/servers/{server_id}/actions#ChangeSeparatePrivateNetworkInterface",
"/v2/servers/{server_id}/actions#ChangeSourceAndDestinationCheck",
"/v2/servers/{server_id}/actions#ChangeThresholdAlerts",
"/v2/servers/{server_id}/actions#ChangeVpcIpv4",
"/v2/servers/{server_id}/actions#CloneUsingBackup",
"/v2/servers/{server_id}/actions#DeleteDisk",
"/v2/servers/{server_id}/actions#DetachBackup",
"/v2/servers/{server_id}/actions#DisableBackups",
"/v2/servers/{server_id}/actions#DisableSelinux",
"/v2/servers/{server_id}/actions#EnableBackups",
"/v2/servers/{server_id}/actions#EnableIpv6",
"/v2/servers/{server_id}/actions#IsRunning",
"/v2/servers/{server_id}/actions#PasswordReset",
"/v2/servers/{server_id}/actions#Ping",
"/v2/servers/{server_id}/actions#PowerCycle",
"/v2/servers/{server_id}/actions#PowerOff",
"/v2/servers/{server_id}/actions#PowerOn",
"/v2/servers/{server_id}/actions#Reboot",
"/v2/servers/{server_id}/actions#Rebuild",
"/v2/servers/{server_id}/actions#Rename",
"/v2/servers/{server_id}/actions#Resize",
"/v2/servers/{server_id}/actions#ResizeDisk",
"/v2/servers/{server_id}/actions#Restore",
"/v2/servers/{server_id}/actions#Shutdown",
"/v2/servers/{server_id}/actions#TakeBackup",
"/v2/servers/{server_id}/actions#Uncancel",
"/v2/servers/{server_id}/actions#Uptime"
]
}
Executable
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
repository_root="$(CDPATH= cd -- "$script_dir/.." && pwd)"
output_directory="${1:-$repository_root/artifacts}"
dotnet pack "$repository_root/src/BinaryLane.Api/BinaryLane.Api.csproj" \
--configuration Release \
--no-restore \
-m:1 \
-nodeReuse:false \
-p:BuildInParallel=false \
--output "$output_directory"
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
project_file="$(CDPATH= cd -- "$script_dir/.." && pwd)/src/BinaryLane.Api/BinaryLane.Api.csproj"
if [[ ! -f "$project_file" ]]; then
printf 'Package project does not exist: %s\n' "$project_file" >&2
exit 2
fi
read_project_property() {
local property_name="$1"
sed -n "s@^[[:space:]]*<${property_name}>[[:space:]]*\\([^<]*\\)[[:space:]]*</${property_name}>.*@\\1@p" \
"$project_file" | head -n 1
}
package_version="$(read_project_property PackageVersion)"
if [[ -z "$package_version" ]]; then
package_version="$(read_project_property Version)"
fi
if [[ -z "$package_version" ]]; then
printf 'Package project must contain a literal <PackageVersion> or <Version>.\n' >&2
exit 2
fi
printf '%s\n' "$package_version"
+57
View File
@@ -0,0 +1,57 @@
#!/usr/bin/env bash
set -euo pipefail
# Downloads the public upstream contract into the reviewed repository snapshot.
# Run this only as part of a human-reviewed change; CI intentionally verifies
# and reports drift instead of changing the snapshot by itself.
script_dir="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
repository_root="$(CDPATH= cd -- "$script_dir/.." && pwd)"
contract_directory="$repository_root/eng/openapi"
contract_file="$contract_directory/binarylane-v2.openapi.yaml"
metadata_file="$contract_directory/contract.json"
source_url="${BINARYLANE_OPENAPI_URL:-https://api.binarylane.com.au/reference/openapi.yaml}"
temporary_file="$(mktemp)"
cleanup() {
rm -f "$temporary_file"
}
trap cleanup EXIT
mkdir -p "$contract_directory"
curl --fail --silent --show-error --location --proto '=https' --proto-redir '=https' \
--connect-timeout 15 --max-time 90 --retry 3 --retry-all-errors \
--output "$temporary_file" \
"$source_url"
if [[ ! -s "$temporary_file" ]]; then
printf 'Downloaded OpenAPI contract is empty.\n' >&2
exit 1
fi
upstream_version="$({
awk '
/^info:$/ { in_info = 1; next }
in_info && /^ version: / {
sub(/^ version: /, "")
gsub(/[[:space:]]+$/, "")
print
exit
}
' "$temporary_file"
} || true)"
if [[ -z "$upstream_version" ]]; then
printf 'Unable to read info.version from the downloaded OpenAPI contract.\n' >&2
exit 1
fi
sha256="$(sha256sum "$temporary_file" | awk '{print $1}')"
retrieved_utc="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
cp "$temporary_file" "$contract_file"
printf '{\n "source": "%s",\n "upstreamVersion": "%s",\n "sha256": "%s",\n "retrievedUtc": "%s"\n}\n' \
"$source_url" "$upstream_version" "$sha256" "$retrieved_utc" > "$metadata_file"
printf 'Updated %s (upstream %s, sha256 %s).\n' \
"$contract_file" "$upstream_version" "$sha256"
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env bash
set -euo pipefail
# Compiles the tracked demo against the nupkg just produced by this repository,
# rather than against the local source project. This catches bad package
# metadata, missing dependencies, and public API regressions.
if [[ $# -ne 1 ]]; then
printf 'Usage: %s <package-directory>\n' "${0##*/}" >&2
exit 2
fi
package_directory="$(CDPATH= cd -- "$1" && pwd)"
script_dir="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
repository_root="$(CDPATH= cd -- "$script_dir/.." && pwd)"
demo_source="$repository_root/examples/BinaryLane.Api.Demo"
temporary_directory="$(mktemp -d)"
export NUGET_PACKAGES="$temporary_directory/nuget-packages"
cleanup() {
rm -rf "$temporary_directory"
}
trap cleanup EXIT
package_file="$(find "$package_directory" -maxdepth 1 -type f -name 'BinaryLane.Api.*.nupkg' -print -quit)"
if [[ -z "$package_file" ]]; then
printf 'No BinaryLane.Api .nupkg found in %s.\n' "$package_directory" >&2
exit 2
fi
package_name="$(basename -- "$package_file")"
package_version="${package_name#BinaryLane.Api.}"
package_version="${package_version%.nupkg}"
demo_directory="$temporary_directory/BinaryLane.Api.Demo"
cp -R "$demo_source" "$demo_directory"
cp "$repository_root/Directory.Build.props" "$temporary_directory/Directory.Build.props"
cp "$repository_root/Directory.Packages.props" "$temporary_directory/Directory.Packages.props"
demo_project="$demo_directory/BinaryLane.Api.Demo.csproj"
if ! grep -Fq '<ProjectReference Include="../../src/BinaryLane.Api/BinaryLane.Api.csproj" />' "$demo_project"; then
printf 'Demo project does not contain the expected local SDK project reference.\n' >&2
exit 2
fi
sed -i "/<\\/ItemGroup>/i\\ <PackageVersion Include=\"BinaryLane.Api\" Version=\"$package_version\" />" \
"$temporary_directory/Directory.Packages.props"
sed -i "s#<ProjectReference Include=\"../../src/BinaryLane.Api/BinaryLane.Api.csproj\" />#<PackageReference Include=\"BinaryLane.Api\" />#" \
"$demo_project"
dotnet restore "$demo_project" \
--source "$package_directory" \
--source https://api.nuget.org/v3/index.json \
--no-cache \
-p:RestoreLockedMode=false
dotnet build "$demo_project" \
--configuration Release \
--no-restore \
-m:1 \
-nodeReuse:false \
-p:BuildInParallel=false
printf 'Demo compiled against BinaryLane.Api %s from %s.\n' \
"$package_version" "$package_directory"
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env bash
set -euo pipefail
# Checks the live public BinaryLane OpenAPI document against the committed
# snapshot. It reports drift but never changes source-controlled files.
script_dir="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
repository_root="$(CDPATH= cd -- "$script_dir/.." && pwd)"
metadata_file="$repository_root/eng/openapi/contract.json"
source_url="${BINARYLANE_OPENAPI_URL:-https://api.binarylane.com.au/reference/openapi.yaml}"
temporary_file="$(mktemp)"
cleanup() {
rm -f "$temporary_file"
}
trap cleanup EXIT
if [[ ! -f "$metadata_file" ]]; then
printf 'Committed contract metadata does not exist: %s\n' "$metadata_file" >&2
exit 2
fi
expected_sha256="$(sed -n 's/^[[:space:]]*"sha256"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$metadata_file")"
expected_version="$(sed -n 's/^[[:space:]]*"upstreamVersion"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$metadata_file")"
if [[ -z "$expected_sha256" || -z "$expected_version" ]]; then
printf 'Committed contract metadata is missing a SHA-256 or upstream version.\n' >&2
exit 2
fi
curl --fail --silent --show-error --location --proto '=https' --proto-redir '=https' \
--connect-timeout 15 --max-time 90 --retry 3 --retry-all-errors \
--output "$temporary_file" \
"$source_url"
if [[ ! -s "$temporary_file" ]]; then
printf 'Downloaded OpenAPI contract is empty.\n' >&2
exit 3
fi
actual_sha256="$(sha256sum "$temporary_file" | awk '{print $1}')"
actual_version="$({
awk '
/^info:$/ { in_info = 1; next }
in_info && /^ version: / {
sub(/^ version: /, "")
gsub(/[[:space:]]+$/, "")
print
exit
}
' "$temporary_file"
} || true)"
if [[ "$expected_sha256" == "$actual_sha256" && "$expected_version" == "$actual_version" ]]; then
printf 'BinaryLane OpenAPI contract matches committed snapshot (%s, %s).\n' \
"$actual_version" "$actual_sha256"
exit 0
fi
printf 'BinaryLane OpenAPI contract changed.\n' >&2
printf ' expected version: %s\n' "$expected_version" >&2
printf ' actual version: %s\n' "${actual_version:-<unreadable>}" >&2
printf ' expected sha256: %s\n' "$expected_sha256" >&2
printf ' actual sha256: %s\n' "$actual_sha256" >&2
printf 'Run ./eng/refresh-openapi-contract.sh, review the diff, then update SDK coverage and tests.\n' >&2
exit 1
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable>
<UserSecretsId>binarylane-api-demo</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Json" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" />
<PackageReference Include="Microsoft.Extensions.Hosting" />
<ProjectReference Include="../../src/BinaryLane.Api/BinaryLane.Api.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>
+167
View File
@@ -0,0 +1,167 @@
using System.Text.Json;
using BinaryLane.Api.V2;
using BinaryLane.Api.V2.DependencyInjection;
using BinaryLane.Api.V2.Errors;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
return await DemoProgram.RunAsync(args);
internal static class DemoProgram
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
WriteIndented = true,
};
public static async Task<int> RunAsync(string[] args)
{
var command = args.FirstOrDefault()?.ToLowerInvariant();
if (command is null or "--help" or "-h" or "help")
{
PrintUsage();
return 0;
}
using var cancellationSource = new CancellationTokenSource();
Console.CancelKeyPress += (_, eventArgs) =>
{
eventArgs.Cancel = true;
cancellationSource.Cancel();
};
var builder = Host.CreateApplicationBuilder(args);
builder.Configuration
.AddJsonFile("appsettings.json", optional: true)
.AddUserSecrets(typeof(DemoProgram).Assembly, optional: true)
.AddEnvironmentVariables();
var token = builder.Configuration["BinaryLane:ApiToken"]
?? Environment.GetEnvironmentVariable("BINARYLANE_API_TOKEN");
if (string.IsNullOrWhiteSpace(token))
{
Console.Error.WriteLine(
"No API token is configured. Use user secrets or set BINARYLANE_API_TOKEN. "
+ "Run with --help for a safe setup command.");
return 2;
}
builder.Services.AddBinaryLaneApi(options =>
{
options.ApiToken = token;
var baseUrl = builder.Configuration["BinaryLane:BaseUrl"];
if (!string.IsNullOrWhiteSpace(baseUrl))
{
options.BaseUrl = baseUrl;
}
});
using var host = builder.Build();
var client = host.Services.GetRequiredService<IBinaryLaneClient>();
try
{
switch (command)
{
case "account":
await WriteJsonAsync(
await client.Account.GetAsync(cancellationSource.Token));
return 0;
case "servers":
await WriteItemsAsync(
client.Servers.ListAllAsync(cancellationToken: cancellationSource.Token));
return 0;
case "server":
if (!TryReadServerId(args, out var serverId))
{
return 2;
}
await WriteJsonAsync(
await client.Servers.GetAsync(serverId, cancellationSource.Token));
return 0;
case "regions":
await WriteItemsAsync(
client.Regions.ListAllAsync(cancellationToken: cancellationSource.Token));
return 0;
default:
Console.Error.WriteLine($"Unknown command: {command}");
PrintUsage();
return 2;
}
}
catch (OperationCanceledException) when (cancellationSource.IsCancellationRequested)
{
Console.Error.WriteLine("Cancelled.");
return 130;
}
catch (BinaryLaneApiException exception)
{
// Provider details may contain user data. Do not print Problem, headers,
// or ResponseBody in a sample application.
Console.Error.WriteLine(
$"BinaryLane request failed with HTTP {(int)exception.StatusCode} "
+ $"({exception.StatusCode}) at {exception.RequestUri.AbsolutePath}.");
return 1;
}
catch (Exception exception)
{
Console.Error.WriteLine($"Request failed ({exception.GetType().Name}).");
return 1;
}
}
private static bool TryReadServerId(string[] args, out long serverId)
{
if (args.Length > 1 && long.TryParse(args[1], out serverId) && serverId > 0)
{
return true;
}
Console.Error.WriteLine("Usage: server <positive-server-id>");
serverId = default;
return false;
}
private static async Task WriteItemsAsync<T>(IAsyncEnumerable<T> items)
{
await foreach (var item in items)
{
await WriteJsonAsync(item);
}
}
private static Task WriteJsonAsync<T>(T value)
{
Console.WriteLine(JsonSerializer.Serialize(value, JsonOptions));
return Task.CompletedTask;
}
private static void PrintUsage()
{
Console.WriteLine(
"""
BinaryLane.Api demo — read-only operations only
Setup:
dotnet user-secrets set "BinaryLane:ApiToken" "your-token" \
--project examples/BinaryLane.Api.Demo
Commands:
account
servers
server <positive-server-id>
regions
You can also set BINARYLANE_API_TOKEN for one process. Never commit
a token or add it to appsettings.json.
""");
}
}
+28
View File
@@ -0,0 +1,28 @@
# BinaryLane.Api demo
This console app demonstrates read-only account, server, and region requests.
It does not create, change, or delete infrastructure.
## Configure a token
From the repository root, store a token with .NET user secrets:
```bash
dotnet user-secrets set \
"BinaryLane:ApiToken" "your-token" \
--project examples/BinaryLane.Api.Demo
```
Alternatively, set `BINARYLANE_API_TOKEN` for one process. Do not add a token
to `appsettings.json`.
## Run it
```bash
dotnet run --project examples/BinaryLane.Api.Demo -- account
dotnet run --project examples/BinaryLane.Api.Demo -- servers
dotnet run --project examples/BinaryLane.Api.Demo -- server 12345
dotnet run --project examples/BinaryLane.Api.Demo -- regions
```
Press Ctrl+C to cancel a request or listing.
@@ -0,0 +1,5 @@
{
"BinaryLane": {
"BaseUrl": "https://api.binarylane.com.au/"
}
}
@@ -0,0 +1,320 @@
{
"version": 2,
"dependencies": {
"net8.0": {
"Microsoft.Extensions.Configuration.Json": {
"type": "Direct",
"requested": "[8.0.1, )",
"resolved": "8.0.1",
"contentHash": "L89DLNuimOghjV3tLx0ArFDwVEJD6+uGB3BMCMX01kaLzXkaXHb2021xOMl2QOxUxbdePKUZsUY7n2UUkycjRg==",
"dependencies": {
"Microsoft.Extensions.Configuration": "8.0.0",
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
"Microsoft.Extensions.Configuration.FileExtensions": "8.0.1",
"Microsoft.Extensions.FileProviders.Abstractions": "8.0.0"
}
},
"Microsoft.Extensions.Configuration.UserSecrets": {
"type": "Direct",
"requested": "[8.0.1, )",
"resolved": "8.0.1",
"contentHash": "7tYqdPPpAK+3jO9d5LTuCK2VxrEdf85Ol4trUr6ds4jclBecadWZ/RyPCbNjfbN5iGTfUnD/h65TOQuqQv2c+A==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
"Microsoft.Extensions.Configuration.Json": "8.0.1",
"Microsoft.Extensions.FileProviders.Abstractions": "8.0.0",
"Microsoft.Extensions.FileProviders.Physical": "8.0.0"
}
},
"Microsoft.Extensions.Hosting": {
"type": "Direct",
"requested": "[8.0.1, )",
"resolved": "8.0.1",
"contentHash": "bP9EEkHBEfjgYiG8nUaXqMk/ujwJrffOkNPP7onpRMO8R+OUSESSP4xHkCAXgYZ1COP2Q9lXlU5gkMFh20gRuw==",
"dependencies": {
"Microsoft.Extensions.Configuration": "8.0.0",
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
"Microsoft.Extensions.Configuration.Binder": "8.0.2",
"Microsoft.Extensions.Configuration.CommandLine": "8.0.0",
"Microsoft.Extensions.Configuration.EnvironmentVariables": "8.0.0",
"Microsoft.Extensions.Configuration.FileExtensions": "8.0.1",
"Microsoft.Extensions.Configuration.Json": "8.0.1",
"Microsoft.Extensions.Configuration.UserSecrets": "8.0.1",
"Microsoft.Extensions.DependencyInjection": "8.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2",
"Microsoft.Extensions.Diagnostics": "8.0.1",
"Microsoft.Extensions.FileProviders.Abstractions": "8.0.0",
"Microsoft.Extensions.FileProviders.Physical": "8.0.0",
"Microsoft.Extensions.Hosting.Abstractions": "8.0.1",
"Microsoft.Extensions.Logging": "8.0.1",
"Microsoft.Extensions.Logging.Abstractions": "8.0.2",
"Microsoft.Extensions.Logging.Configuration": "8.0.1",
"Microsoft.Extensions.Logging.Console": "8.0.1",
"Microsoft.Extensions.Logging.Debug": "8.0.1",
"Microsoft.Extensions.Logging.EventLog": "8.0.1",
"Microsoft.Extensions.Logging.EventSource": "8.0.1",
"Microsoft.Extensions.Options": "8.0.2"
}
},
"Microsoft.Extensions.Configuration": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "0J/9YNXTMWSZP2p2+nvl8p71zpSwokZXZuJW+VjdErkegAnFdO1XlqtA62SJtgVYHdKu3uPxJHcMR/r35HwFBA==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
"Microsoft.Extensions.Primitives": "8.0.0"
}
},
"Microsoft.Extensions.Configuration.Abstractions": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "3lE/iLSutpgX1CC0NOW70FJoGARRHbyKmG7dc0klnUZ9Dd9hS6N/POPWhKhMLCEuNN5nXEY5agmlFtH562vqhQ==",
"dependencies": {
"Microsoft.Extensions.Primitives": "8.0.0"
}
},
"Microsoft.Extensions.Configuration.CommandLine": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "NZuZMz3Q8Z780nKX3ifV1fE7lS+6pynDHK71OfU4OZ1ItgvDOhyOC7E6z+JMZrAj63zRpwbdldYFk499t3+1dQ==",
"dependencies": {
"Microsoft.Extensions.Configuration": "8.0.0",
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0"
}
},
"Microsoft.Extensions.Configuration.EnvironmentVariables": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "plvZ0ZIpq+97gdPNNvhwvrEZ92kNml9hd1pe3idMA7svR0PztdzVLkoWLcRFgySYXUJc3kSM3Xw3mNFMo/bxRA==",
"dependencies": {
"Microsoft.Extensions.Configuration": "8.0.0",
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0"
}
},
"Microsoft.Extensions.Configuration.FileExtensions": {
"type": "Transitive",
"resolved": "8.0.1",
"contentHash": "EJzSNO9oaAXnTdtdNO6npPRsIIeZCBSNmdQ091VDO7fBiOtJAAeEq6dtrVXIi3ZyjC5XRSAtVvF8SzcneRHqKQ==",
"dependencies": {
"Microsoft.Extensions.Configuration": "8.0.0",
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
"Microsoft.Extensions.FileProviders.Abstractions": "8.0.0",
"Microsoft.Extensions.FileProviders.Physical": "8.0.0",
"Microsoft.Extensions.Primitives": "8.0.0"
}
},
"Microsoft.Extensions.Diagnostics": {
"type": "Transitive",
"resolved": "8.0.1",
"contentHash": "doVPCUUCY7c6LhBsEfiy3W1bvS7Mi6LkfQMS8nlC22jZWNxBv8VO8bdfeyvpYFst6Kxqk7HBC6lytmEoBssvSQ==",
"dependencies": {
"Microsoft.Extensions.Configuration": "8.0.0",
"Microsoft.Extensions.Diagnostics.Abstractions": "8.0.1",
"Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0"
}
},
"Microsoft.Extensions.Diagnostics.Abstractions": {
"type": "Transitive",
"resolved": "8.0.1",
"contentHash": "elH2vmwNmsXuKmUeMQ4YW9ldXiF+gSGDgg1vORksob5POnpaI6caj1Hu8zaYbEuibhqCoWg0YRWDazBY3zjBfg==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2",
"Microsoft.Extensions.Options": "8.0.2"
}
},
"Microsoft.Extensions.FileProviders.Abstractions": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "ZbaMlhJlpisjuWbvXr4LdAst/1XxH3vZ6A0BsgTphZ2L4PGuxRLz7Jr/S7mkAAnOn78Vu0fKhEgNF5JO3zfjqQ==",
"dependencies": {
"Microsoft.Extensions.Primitives": "8.0.0"
}
},
"Microsoft.Extensions.FileProviders.Physical": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "UboiXxpPUpwulHvIAVE36Knq0VSHaAmfrFkegLyBZeaADuKezJ/AIXYAW8F5GBlGk/VaibN2k/Zn1ca8YAfVdA==",
"dependencies": {
"Microsoft.Extensions.FileProviders.Abstractions": "8.0.0",
"Microsoft.Extensions.FileSystemGlobbing": "8.0.0",
"Microsoft.Extensions.Primitives": "8.0.0"
}
},
"Microsoft.Extensions.FileSystemGlobbing": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "OK+670i7esqlQrPjdIKRbsyMCe9g5kSLpRRQGSr4Q58AOYEe/hCnfLZprh7viNisSUUQZmMrbbuDaIrP+V1ebQ=="
},
"Microsoft.Extensions.Hosting.Abstractions": {
"type": "Transitive",
"resolved": "8.0.1",
"contentHash": "nHwq9aPBdBPYXPti6wYEEfgXddfBrYC+CQLn+qISiwQq5tpfaqDZSKOJNxoe9rfQxGf1c+2wC/qWFe1QYJPYqw==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2",
"Microsoft.Extensions.Diagnostics.Abstractions": "8.0.1",
"Microsoft.Extensions.FileProviders.Abstractions": "8.0.0",
"Microsoft.Extensions.Logging.Abstractions": "8.0.2"
}
},
"Microsoft.Extensions.Logging": {
"type": "Transitive",
"resolved": "8.0.1",
"contentHash": "4x+pzsQEbqxhNf1QYRr5TDkLP9UsLT3A6MdRKDDEgrW7h1ljiEPgTNhKYUhNCCAaVpQECVQ+onA91PTPnIp6Lw==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "8.0.1",
"Microsoft.Extensions.Logging.Abstractions": "8.0.2",
"Microsoft.Extensions.Options": "8.0.2"
}
},
"Microsoft.Extensions.Logging.Abstractions": {
"type": "Transitive",
"resolved": "8.0.2",
"contentHash": "nroMDjS7hNBPtkZqVBbSiQaQjWRDxITI8Y7XnDs97rqG3EbzVTNLZQf7bIeUJcaHOV8bca47s1Uxq94+2oGdxA==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2"
}
},
"Microsoft.Extensions.Logging.Configuration": {
"type": "Transitive",
"resolved": "8.0.1",
"contentHash": "QWwTrsgOnJMmn+XUslm8D2H1n3PkP/u/v52FODtyBc/k4W9r3i2vcXXeeX/upnzllJYRRbrzVzT0OclfNJtBJA==",
"dependencies": {
"Microsoft.Extensions.Configuration": "8.0.0",
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
"Microsoft.Extensions.Configuration.Binder": "8.0.2",
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2",
"Microsoft.Extensions.Logging": "8.0.1",
"Microsoft.Extensions.Logging.Abstractions": "8.0.2",
"Microsoft.Extensions.Options": "8.0.2",
"Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0"
}
},
"Microsoft.Extensions.Logging.Console": {
"type": "Transitive",
"resolved": "8.0.1",
"contentHash": "uzcg/5U2eLyn5LIKlERkdSxw6VPC1yydnOSQiRRWGBGN3kphq3iL4emORzrojScDmxRhv49gp5BI8U3Dz7y4iA==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2",
"Microsoft.Extensions.Logging": "8.0.1",
"Microsoft.Extensions.Logging.Abstractions": "8.0.2",
"Microsoft.Extensions.Logging.Configuration": "8.0.1",
"Microsoft.Extensions.Options": "8.0.2"
}
},
"Microsoft.Extensions.Logging.Debug": {
"type": "Transitive",
"resolved": "8.0.1",
"contentHash": "B8hqNuYudC2RB+L/DI33uO4rf5by41fZVdcVL2oZj0UyoAZqnwTwYHp1KafoH4nkl1/23piNeybFFASaV2HkFg==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2",
"Microsoft.Extensions.Logging": "8.0.1",
"Microsoft.Extensions.Logging.Abstractions": "8.0.2"
}
},
"Microsoft.Extensions.Logging.EventLog": {
"type": "Transitive",
"resolved": "8.0.1",
"contentHash": "ZD1m4GXoxcZeDJIq8qePKj+QAWeQNO/OG8skvrOG8RQfxLp9MAKRoliTc27xanoNUzeqvX5HhS/I7c0BvwAYUg==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2",
"Microsoft.Extensions.Logging": "8.0.1",
"Microsoft.Extensions.Logging.Abstractions": "8.0.2",
"Microsoft.Extensions.Options": "8.0.2",
"System.Diagnostics.EventLog": "8.0.1"
}
},
"Microsoft.Extensions.Logging.EventSource": {
"type": "Transitive",
"resolved": "8.0.1",
"contentHash": "YMXMAla6B6sEf/SnfZYTty633Ool3AH7KOw2LOaaEqwSo2piK4f7HMtzyc3CNiipDnq1fsUSuG5Oc7ZzpVy8WQ==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2",
"Microsoft.Extensions.Logging": "8.0.1",
"Microsoft.Extensions.Logging.Abstractions": "8.0.2",
"Microsoft.Extensions.Options": "8.0.2",
"Microsoft.Extensions.Primitives": "8.0.0"
}
},
"Microsoft.Extensions.Primitives": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g=="
},
"System.Diagnostics.EventLog": {
"type": "Transitive",
"resolved": "8.0.1",
"contentHash": "n1ZP7NM2Gkn/MgD8+eOT5MulMj6wfeQMNS2Pizvq5GHCZfjlFMXV2irQlQmJhwA2VABC57M0auudO89Iu2uRLg=="
},
"binarylane.api": {
"type": "Project",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "[8.0.2, )",
"Microsoft.Extensions.Http": "[8.0.1, )",
"Microsoft.Extensions.Options": "[8.0.2, )"
}
},
"Microsoft.Extensions.Configuration.Binder": {
"type": "CentralTransitive",
"requested": "[8.0.2, )",
"resolved": "8.0.2",
"contentHash": "7IQhGK+wjyGrNsPBjJcZwWAr+Wf6D4+TwOptUt77bWtgNkiV8tDEbhFS+dDamtQFZ2X7kWG9m71iZQRj2x3zgQ==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0"
}
},
"Microsoft.Extensions.DependencyInjection": {
"type": "CentralTransitive",
"requested": "[8.0.1, )",
"resolved": "8.0.1",
"contentHash": "BmANAnR5Xd4Oqw7yQ75xOAYODybZQRzdeNucg7kS5wWKd2PNnMdYtJ2Vciy0QLylRmv42DGl5+AFL9izA6F1Rw==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2"
}
},
"Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "CentralTransitive",
"requested": "[8.0.2, )",
"resolved": "8.0.2",
"contentHash": "3iE7UF7MQkCv1cxzCahz+Y/guQbTqieyxyaWKhrRO91itI9cOKO76OHeQDahqG4MmW5umr3CcCvGmK92lWNlbg=="
},
"Microsoft.Extensions.Http": {
"type": "CentralTransitive",
"requested": "[8.0.1, )",
"resolved": "8.0.1",
"contentHash": "kDYeKJUzh0qeg/AI+nSr3ffthmXYQTEb0nS9qRC7YhSbbuN4M4NPbaB77AJwtkTnCV9XZ7qYj3dkZaNcyl73EA==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2",
"Microsoft.Extensions.Diagnostics": "8.0.1",
"Microsoft.Extensions.Logging": "8.0.1",
"Microsoft.Extensions.Logging.Abstractions": "8.0.2",
"Microsoft.Extensions.Options": "8.0.2"
}
},
"Microsoft.Extensions.Options": {
"type": "CentralTransitive",
"requested": "[8.0.2, )",
"resolved": "8.0.2",
"contentHash": "dWGKvhFybsaZpGmzkGCbNNwBD1rVlWzrZKANLW/CcbFJpCEceMCGzT7zZwHOGBCbwM0SzBuceMj5HN1LKV1QqA==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
"Microsoft.Extensions.Primitives": "8.0.0"
}
},
"Microsoft.Extensions.Options.ConfigurationExtensions": {
"type": "CentralTransitive",
"requested": "[8.0.0, )",
"resolved": "8.0.0",
"contentHash": "0f4DMRqEd50zQh+UyJc+/HiBsZ3vhAQALgdkcQEalSH1L2isdC7Yj54M3cyo5e+BeO5fcBQ7Dxly8XiBBcvRgw==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
"Microsoft.Extensions.Configuration.Binder": "8.0.0",
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
"Microsoft.Extensions.Options": "8.0.0",
"Microsoft.Extensions.Primitives": "8.0.0"
}
}
}
}
}
+7
View File
@@ -0,0 +1,7 @@
{
"sdk": {
"version": "10.0.302",
"rollForward": "disable",
"allowPrerelease": false
}
}
@@ -0,0 +1,11 @@
using System.Threading;
using System.Threading.Tasks;
namespace BinaryLane.Api.V2.Authentication;
/// <summary>Supplies a BinaryLane bearer token for an outgoing request.</summary>
public interface IBinaryLaneTokenProvider
{
/// <summary>Gets the current bearer token.</summary>
ValueTask<string> GetTokenAsync(CancellationToken cancellationToken = default);
}
@@ -0,0 +1,30 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using BinaryLane.Api.V2.Configuration;
using Microsoft.Extensions.Options;
namespace BinaryLane.Api.V2.Authentication;
/// <summary>Uses the token configured in <see cref="BinaryLaneOptions"/>.</summary>
public sealed class OptionsBinaryLaneTokenProvider : IBinaryLaneTokenProvider
{
private readonly IOptionsMonitor<BinaryLaneOptions> _options;
/// <summary>Initializes the provider.</summary>
public OptionsBinaryLaneTokenProvider(IOptionsMonitor<BinaryLaneOptions> options) =>
_options = options ?? throw new ArgumentNullException(nameof(options));
/// <inheritdoc />
public ValueTask<string> GetTokenAsync(CancellationToken cancellationToken = default)
{
var token = _options.CurrentValue.ApiToken;
if (string.IsNullOrWhiteSpace(token))
{
throw new InvalidOperationException(
"No BinaryLane API token is configured. Set BinaryLaneOptions.ApiToken or register a custom IBinaryLaneTokenProvider.");
}
return new ValueTask<string>(token!);
}
}
@@ -0,0 +1,26 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace BinaryLane.Api.V2.Authentication;
/// <summary>Provides a fixed token. Prefer a custom provider when tokens rotate.</summary>
public sealed class StaticBinaryLaneTokenProvider : IBinaryLaneTokenProvider
{
private readonly string _token;
/// <summary>Initializes the provider with a non-empty bearer token.</summary>
public StaticBinaryLaneTokenProvider(string token)
{
if (string.IsNullOrWhiteSpace(token))
{
throw new ArgumentException("A BinaryLane API token is required.", nameof(token));
}
_token = token;
}
/// <inheritdoc />
public ValueTask<string> GetTokenAsync(CancellationToken cancellationToken = default) =>
new(_token);
}
+48
View File
@@ -0,0 +1,48 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;netstandard2.0</TargetFrameworks>
<PackageId>BinaryLane.Api</PackageId>
<AssemblyName>BinaryLane.Api</AssemblyName>
<RootNamespace>BinaryLane.Api.V2</RootNamespace>
<Version>0.1.0-beta.1</Version>
<Authors>Alex Hope-O'Connor</Authors>
<Owners>alex.hope.oconnor</Owners>
<Copyright>Copyright © 2026 Alex Hope-O'Connor</Copyright>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
<IsPackable>true</IsPackable>
<Description>Unofficial, typed .NET client for BinaryLane's v2 API.</Description>
<PackageTags>binarylane;api;cloud;vps;dns</PackageTags>
<PackageProjectUrl>https://github.com/alexhopeoconnor/binarylane-dotnet</PackageProjectUrl>
<RepositoryUrl>https://github.com/alexhopeoconnor/binarylane-dotnet</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageIcon>icon.png</PackageIcon>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<!-- Pack must serialize multi-target nuspec and symbols generation. -->
<BuildInParallel>false</BuildInParallel>
</PropertyGroup>
<!-- Versions are centrally managed by Directory.Packages.props. -->
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Http" />
<PackageReference Include="Microsoft.Extensions.Options" />
<PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="all" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" />
<PackageReference Include="System.Text.Json" />
</ItemGroup>
<ItemGroup>
<None Include="../../README.md" Pack="true" PackagePath="/" Link="README.md" />
<None Include="../../LICENSE" Pack="true" PackagePath="/" Link="LICENSE" />
<None Include="icon.png" Pack="true" PackagePath="/" />
</ItemGroup>
</Project>
+117
View File
@@ -0,0 +1,117 @@
using System;
using System.Net.Http;
using BinaryLane.Api.V2.Authentication;
using BinaryLane.Api.V2.Http;
using BinaryLane.Api.V2.Resources;
using Microsoft.Extensions.DependencyInjection;
namespace BinaryLane.Api.V2;
/// <summary>Default composable BinaryLane v2 client.</summary>
public sealed class BinaryLaneClient : IBinaryLaneClient
{
/// <summary>
/// Creates a client using the supplied HTTP client and token provider. The HTTP client should
/// normally be obtained through <c>IHttpClientFactory</c> or registered with
/// <c>AddBinaryLaneApi</c>.
/// </summary>
public BinaryLaneClient(HttpClient httpClient, IBinaryLaneTokenProvider tokenProvider)
: this(httpClient, tokenProvider, new BinaryLaneJsonSerializerOptions())
{
}
/// <summary>Creates a client using supplied HTTP, authentication, and JSON services.</summary>
[ActivatorUtilitiesConstructor]
public BinaryLaneClient(
HttpClient httpClient,
IBinaryLaneTokenProvider tokenProvider,
BinaryLaneJsonSerializerOptions json)
: this(new BinaryLaneHttpExecutor(httpClient, tokenProvider, json), json)
{
}
/// <summary>Creates a client over a custom request executor.</summary>
public BinaryLaneClient(IBinaryLaneApiExecutor executor, BinaryLaneJsonSerializerOptions json)
{
#if NET8_0_OR_GREATER
ArgumentNullException.ThrowIfNull(executor);
ArgumentNullException.ThrowIfNull(json);
#else
if (executor is null)
{
throw new ArgumentNullException(nameof(executor));
}
if (json is null)
{
throw new ArgumentNullException(nameof(json));
}
#endif
Executor = executor;
Account = new AccountApi(executor, json);
Actions = new ActionsApi(executor, json);
Billing = new BillingApi(executor, json);
DataUsage = new DataUsageApi(executor, json);
Domains = new DomainsApi(executor, json);
Images = new ImagesApi(executor, json);
SshKeys = new SshKeysApi(executor, json);
LoadBalancers = new LoadBalancersApi(executor, json);
Regions = new RegionsApi(executor, json);
ReverseNames = new ReverseNamesApi(executor, json);
SampleSets = new SampleSetsApi(executor, json);
Servers = new ServersApi(executor, json);
Sizes = new SizesApi(executor, json);
Software = new SoftwareApi(executor, json);
Vpcs = new VpcsApi(executor, json);
}
/// <inheritdoc />
public IBinaryLaneApiExecutor Executor { get; }
/// <inheritdoc />
public IAccountApi Account { get; }
/// <inheritdoc />
public IActionsApi Actions { get; }
/// <inheritdoc />
public IBillingApi Billing { get; }
/// <inheritdoc />
public IDataUsageApi DataUsage { get; }
/// <inheritdoc />
public IDomainsApi Domains { get; }
/// <inheritdoc />
public IImagesApi Images { get; }
/// <inheritdoc />
public ISshKeysApi SshKeys { get; }
/// <inheritdoc />
public ILoadBalancersApi LoadBalancers { get; }
/// <inheritdoc />
public IRegionsApi Regions { get; }
/// <inheritdoc />
public IReverseNamesApi ReverseNames { get; }
/// <inheritdoc />
public ISampleSetsApi SampleSets { get; }
/// <inheritdoc />
public IServersApi Servers { get; }
/// <inheritdoc />
public ISizesApi Sizes { get; }
/// <inheritdoc />
public ISoftwareApi Software { get; }
/// <inheritdoc />
public IVpcsApi Vpcs { get; }
}
@@ -0,0 +1,36 @@
using System;
namespace BinaryLane.Api.V2.Configuration;
/// <summary>Configures a BinaryLane API client.</summary>
public sealed class BinaryLaneOptions
{
/// <summary>The configuration section convention used by consuming applications.</summary>
public const string SectionName = "BinaryLane";
/// <summary>The BinaryLane API root URL.</summary>
public string BaseUrl { get; set; } = "https://api.binarylane.com.au/";
/// <summary>
/// The bearer token to use when the default token provider is registered. Prefer a secret store,
/// user secrets, or environment-variable based configuration rather than committing this value.
/// </summary>
public string? ApiToken { get; set; }
/// <summary>Timeout applied to individual HTTP requests.</summary>
public int RequestTimeoutSeconds { get; set; } = 100;
internal static bool IsValid(BinaryLaneOptions options)
{
if (options.RequestTimeoutSeconds < 1 || options.RequestTimeoutSeconds > 300)
{
return false;
}
return Uri.TryCreate(options.BaseUrl, UriKind.Absolute, out var uri)
&& uri.Scheme == Uri.UriSchemeHttps
&& string.IsNullOrEmpty(uri.UserInfo)
&& string.IsNullOrEmpty(uri.Query)
&& string.IsNullOrEmpty(uri.Fragment);
}
}
@@ -0,0 +1,24 @@
using Microsoft.Extensions.Options;
namespace BinaryLane.Api.V2.Configuration;
/// <summary>Validates client options at application startup.</summary>
public sealed class BinaryLaneOptionsValidator : IValidateOptions<BinaryLaneOptions>
{
/// <inheritdoc />
public ValidateOptionsResult Validate(string? name, BinaryLaneOptions options)
{
if (options is null)
{
return ValidateOptionsResult.Fail("BinaryLane options are required.");
}
if (!BinaryLaneOptions.IsValid(options))
{
return ValidateOptionsResult.Fail(
"BinaryLane BaseUrl must be an absolute HTTPS URL and RequestTimeoutSeconds must be between 1 and 300.");
}
return ValidateOptionsResult.Success;
}
}
@@ -0,0 +1,81 @@
using System;
using BinaryLane.Api.V2.Authentication;
using BinaryLane.Api.V2.Configuration;
using BinaryLane.Api.V2.Http;
using BinaryLane.Api.V2.Resources;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
namespace BinaryLane.Api.V2.DependencyInjection;
/// <summary>Dependency-injection registration for the BinaryLane API client.</summary>
public static class ServiceCollectionExtensions
{
/// <summary>
/// Registers a typed BinaryLane client. The returned builder lets an application compose its
/// own proxy, telemetry, and safe GET-only resilience handlers.
/// </summary>
public static IHttpClientBuilder AddBinaryLaneApi(
this IServiceCollection services,
Action<BinaryLaneOptions> configure)
{
#if NET8_0_OR_GREATER
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(configure);
#else
if (services is null)
{
throw new ArgumentNullException(nameof(services));
}
if (configure is null)
{
throw new ArgumentNullException(nameof(configure));
}
#endif
services.AddOptions<BinaryLaneOptions>()
.Configure(configure)
.ValidateOnStart();
services.TryAddSingleton<IValidateOptions<BinaryLaneOptions>, BinaryLaneOptionsValidator>();
services.TryAddSingleton<IBinaryLaneTokenProvider, OptionsBinaryLaneTokenProvider>();
services.TryAddSingleton<BinaryLaneJsonSerializerOptions>();
var clientBuilder = services.AddHttpClient<IBinaryLaneClient, BinaryLaneClient>((serviceProvider, client) =>
{
var options = serviceProvider.GetRequiredService<IOptions<BinaryLaneOptions>>().Value;
client.BaseAddress = EnsureTrailingSlash(options.BaseUrl);
client.Timeout = TimeSpan.FromSeconds(options.RequestTimeoutSeconds);
});
// Register focused resources as well as the façade so applications can depend on precisely
// the API area they use. TryAdd preserves a consumer's explicit test double or custom resource.
services.TryAddTransient<IAccountApi>(serviceProvider => serviceProvider.GetRequiredService<IBinaryLaneClient>().Account);
services.TryAddTransient<IActionsApi>(serviceProvider => serviceProvider.GetRequiredService<IBinaryLaneClient>().Actions);
services.TryAddTransient<IBillingApi>(serviceProvider => serviceProvider.GetRequiredService<IBinaryLaneClient>().Billing);
services.TryAddTransient<IDataUsageApi>(serviceProvider => serviceProvider.GetRequiredService<IBinaryLaneClient>().DataUsage);
services.TryAddTransient<IDomainsApi>(serviceProvider => serviceProvider.GetRequiredService<IBinaryLaneClient>().Domains);
services.TryAddTransient<IImagesApi>(serviceProvider => serviceProvider.GetRequiredService<IBinaryLaneClient>().Images);
services.TryAddTransient<ISshKeysApi>(serviceProvider => serviceProvider.GetRequiredService<IBinaryLaneClient>().SshKeys);
services.TryAddTransient<ILoadBalancersApi>(serviceProvider => serviceProvider.GetRequiredService<IBinaryLaneClient>().LoadBalancers);
services.TryAddTransient<IRegionsApi>(serviceProvider => serviceProvider.GetRequiredService<IBinaryLaneClient>().Regions);
services.TryAddTransient<IReverseNamesApi>(serviceProvider => serviceProvider.GetRequiredService<IBinaryLaneClient>().ReverseNames);
services.TryAddTransient<ISampleSetsApi>(serviceProvider => serviceProvider.GetRequiredService<IBinaryLaneClient>().SampleSets);
services.TryAddTransient<IServersApi>(serviceProvider => serviceProvider.GetRequiredService<IBinaryLaneClient>().Servers);
services.TryAddTransient<ISizesApi>(serviceProvider => serviceProvider.GetRequiredService<IBinaryLaneClient>().Sizes);
services.TryAddTransient<ISoftwareApi>(serviceProvider => serviceProvider.GetRequiredService<IBinaryLaneClient>().Software);
services.TryAddTransient<IVpcsApi>(serviceProvider => serviceProvider.GetRequiredService<IBinaryLaneClient>().Vpcs);
return clientBuilder;
}
private static Uri EnsureTrailingSlash(string baseUrl)
{
var uri = new Uri(baseUrl, UriKind.Absolute);
var absoluteUri = uri.AbsoluteUri;
return absoluteUri.Length > 0 && absoluteUri[absoluteUri.Length - 1] == '/'
? uri
: new Uri(absoluteUri + "/", UriKind.Absolute);
}
}
@@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.Net;
namespace BinaryLane.Api.V2.Errors;
/// <summary>Base exception for an unsuccessful BinaryLane API response.</summary>
public class BinaryLaneApiException : Exception
{
internal BinaryLaneApiException(
string message,
HttpStatusCode statusCode,
Uri requestUri,
BinaryLaneApiProblem? problem,
string? responseBody,
IReadOnlyDictionary<string, IReadOnlyList<string>> headers)
: base(message)
{
StatusCode = statusCode;
RequestUri = requestUri;
Problem = problem;
ResponseBody = responseBody;
Headers = headers;
}
/// <summary>HTTP status code returned by BinaryLane.</summary>
public HttpStatusCode StatusCode { get; }
/// <summary>The request URI which produced the response.</summary>
public Uri RequestUri { get; }
/// <summary>Structured problem details, when BinaryLane supplied them.</summary>
public BinaryLaneApiProblem? Problem { get; }
/// <summary>
/// Bounded raw response text for diagnostics. It omits request headers, but can contain
/// provider-supplied or user-provided data; avoid logging it indiscriminately.
/// </summary>
public string? ResponseBody { get; }
/// <summary>Response and content headers.</summary>
public IReadOnlyDictionary<string, IReadOnlyList<string>> Headers { get; }
}
@@ -0,0 +1,17 @@
namespace BinaryLane.Api.V2.Errors;
/// <summary>A structured problem response returned by BinaryLane.</summary>
public sealed class BinaryLaneApiProblem
{
/// <summary>Problem type URI, if supplied.</summary>
public string? Type { get; internal set; }
/// <summary>Short problem title, if supplied.</summary>
public string? Title { get; internal set; }
/// <summary>Human-readable problem detail, if supplied.</summary>
public string? Detail { get; internal set; }
/// <summary>Provider-supplied status code, if supplied.</summary>
public int? Status { get; internal set; }
}
@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Net;
namespace BinaryLane.Api.V2.Errors;
/// <summary>Thrown when the current account is not authorized for an operation.</summary>
public sealed class BinaryLaneForbiddenException : BinaryLaneApiException
{
internal BinaryLaneForbiddenException(string message, Uri requestUri, BinaryLaneApiProblem? problem, string? responseBody, IReadOnlyDictionary<string, IReadOnlyList<string>> headers)
: base(message, HttpStatusCode.Forbidden, requestUri, problem, responseBody, headers)
{
}
}
@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Net;
namespace BinaryLane.Api.V2.Errors;
/// <summary>Thrown when BinaryLane cannot find a requested resource.</summary>
public sealed class BinaryLaneNotFoundException : BinaryLaneApiException
{
internal BinaryLaneNotFoundException(string message, Uri requestUri, BinaryLaneApiProblem? problem, string? responseBody, IReadOnlyDictionary<string, IReadOnlyList<string>> headers)
: base(message, HttpStatusCode.NotFound, requestUri, problem, responseBody, headers)
{
}
}
@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Net;
namespace BinaryLane.Api.V2.Errors;
/// <summary>Thrown when BinaryLane rejects credentials.</summary>
public sealed class BinaryLaneUnauthorizedException : BinaryLaneApiException
{
internal BinaryLaneUnauthorizedException(string message, Uri requestUri, BinaryLaneApiProblem? problem, string? responseBody, IReadOnlyDictionary<string, IReadOnlyList<string>> headers)
: base(message, HttpStatusCode.Unauthorized, requestUri, problem, responseBody, headers)
{
}
}
@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Net;
namespace BinaryLane.Api.V2.Errors;
/// <summary>Thrown when BinaryLane rejects a malformed or invalid request.</summary>
public sealed class BinaryLaneValidationException : BinaryLaneApiException
{
internal BinaryLaneValidationException(string message, HttpStatusCode statusCode, Uri requestUri, BinaryLaneApiProblem? problem, string? responseBody, IReadOnlyDictionary<string, IReadOnlyList<string>> headers)
: base(message, statusCode, requestUri, problem, responseBody, headers)
{
}
}
@@ -0,0 +1,492 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using BinaryLane.Api.V2.Authentication;
using BinaryLane.Api.V2.Errors;
namespace BinaryLane.Api.V2.Http;
/// <summary>Default HTTP implementation for the BinaryLane API.</summary>
public sealed class BinaryLaneHttpExecutor : IBinaryLaneApiExecutor
{
private const int MaximumDiagnosticBodyLength = 32 * 1024;
private const long MaximumSuccessBodyLength = 16L * 1024 * 1024;
private static readonly Uri DefaultBaseUri = new("https://api.binarylane.com.au/");
private readonly HttpClient _httpClient;
private readonly IBinaryLaneTokenProvider _tokenProvider;
private readonly BinaryLaneJsonSerializerOptions _json;
/// <summary>Initializes the executor.</summary>
public BinaryLaneHttpExecutor(
HttpClient httpClient,
IBinaryLaneTokenProvider tokenProvider,
BinaryLaneJsonSerializerOptions? json = null)
{
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
_tokenProvider = tokenProvider ?? throw new ArgumentNullException(nameof(tokenProvider));
_json = json ?? new BinaryLaneJsonSerializerOptions();
}
/// <inheritdoc />
public async Task<BinaryLaneResponse<TResponse>> SendAsync<TResponse>(
BinaryLaneRequest request,
CancellationToken cancellationToken = default)
{
#if NET8_0_OR_GREATER
ArgumentNullException.ThrowIfNull(request);
#else
if (request is null)
{
throw new ArgumentNullException(nameof(request));
}
#endif
using var message = await CreateMessageAsync(request, cancellationToken).ConfigureAwait(false);
using var response = await _httpClient
.SendAsync(message, HttpCompletionOption.ResponseHeadersRead, cancellationToken)
.ConfigureAwait(false);
var headers = CollectHeaders(response);
var body = response.Content is null
? string.Empty
: response.IsSuccessStatusCode
? await ReadBodyAsync(response.Content, cancellationToken).ConfigureAwait(false)
: await ReadDiagnosticBodyAsync(response.Content, cancellationToken).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
throw CreateException(response.StatusCode, message.RequestUri!, body, headers);
}
var result = Deserialize<TResponse>(body);
return new BinaryLaneResponse<TResponse>(result, response.StatusCode, message.RequestUri!, headers);
}
/// <inheritdoc />
public async Task<BinaryLaneResponse<object?>> SendAsync(
BinaryLaneRequest request,
CancellationToken cancellationToken = default)
{
#if NET8_0_OR_GREATER
ArgumentNullException.ThrowIfNull(request);
#else
if (request is null)
{
throw new ArgumentNullException(nameof(request));
}
#endif
using var message = await CreateMessageAsync(request, cancellationToken).ConfigureAwait(false);
using var response = await _httpClient
.SendAsync(message, HttpCompletionOption.ResponseHeadersRead, cancellationToken)
.ConfigureAwait(false);
var headers = CollectHeaders(response);
var body = response.Content is null
? string.Empty
: response.IsSuccessStatusCode
? await ReadBodyAsync(response.Content, cancellationToken).ConfigureAwait(false)
: await ReadDiagnosticBodyAsync(response.Content, cancellationToken).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
throw CreateException(response.StatusCode, message.RequestUri!, body, headers);
}
return new BinaryLaneResponse<object?>(null, response.StatusCode, message.RequestUri!, headers);
}
/// <inheritdoc />
public async Task<TResponse> GetAsync<TResponse>(
string path,
IReadOnlyDictionary<string, object?>? query = null,
CancellationToken cancellationToken = default) =>
(await SendAsync<TResponse>(new BinaryLaneRequest(HttpMethod.Get, path) { Query = query }, cancellationToken)
.ConfigureAwait(false)).Body;
/// <inheritdoc />
public async Task<TResponse> PostAsync<TResponse>(
string path,
object? body = null,
IReadOnlyDictionary<string, object?>? query = null,
CancellationToken cancellationToken = default) =>
(await SendAsync<TResponse>(new BinaryLaneRequest(HttpMethod.Post, path) { Body = body, Query = query }, cancellationToken)
.ConfigureAwait(false)).Body;
/// <inheritdoc />
public async Task<TResponse> PutAsync<TResponse>(
string path,
object? body = null,
IReadOnlyDictionary<string, object?>? query = null,
CancellationToken cancellationToken = default) =>
(await SendAsync<TResponse>(new BinaryLaneRequest(HttpMethod.Put, path) { Body = body, Query = query }, cancellationToken)
.ConfigureAwait(false)).Body;
/// <inheritdoc />
public async Task<TResponse> PatchAsync<TResponse>(
string path,
object? body = null,
IReadOnlyDictionary<string, object?>? query = null,
CancellationToken cancellationToken = default) =>
(await SendAsync<TResponse>(new BinaryLaneRequest(new HttpMethod("PATCH"), path) { Body = body, Query = query }, cancellationToken)
.ConfigureAwait(false)).Body;
/// <inheritdoc />
public async Task DeleteAsync(
string path,
object? body = null,
IReadOnlyDictionary<string, object?>? query = null,
CancellationToken cancellationToken = default)
{
await SendAsync(new BinaryLaneRequest(HttpMethod.Delete, path) { Body = body, Query = query }, cancellationToken)
.ConfigureAwait(false);
}
private async Task<HttpRequestMessage> CreateMessageAsync(BinaryLaneRequest request, CancellationToken cancellationToken)
{
var requestUri = CreateUri(request.Path, request.Query);
var token = await _tokenProvider.GetTokenAsync(cancellationToken).ConfigureAwait(false);
if (string.IsNullOrWhiteSpace(token))
{
throw new InvalidOperationException("The configured BinaryLane token provider returned an empty token.");
}
var message = new HttpRequestMessage(request.Method, requestUri);
try
{
if (request.Headers is not null)
{
foreach (var header in request.Headers)
{
if (string.Equals(header.Key, "Authorization", StringComparison.OrdinalIgnoreCase) ||
string.Equals(header.Key, "Host", StringComparison.OrdinalIgnoreCase))
{
throw new ArgumentException(
$"The '{header.Key}' header is managed by the BinaryLane client and cannot be overridden.",
nameof(request));
}
try
{
message.Headers.Add(header.Key, header.Value);
}
catch (Exception exception) when (exception is FormatException or InvalidOperationException)
{
throw new ArgumentException(
$"The '{header.Key}' header is not a valid request header for BinaryLane API requests.",
nameof(request),
exception);
}
}
}
message.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
message.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
if (request.Body is not null)
{
var json = JsonSerializer.Serialize(request.Body, _json.SerializerOptions);
message.Content = new StringContent(json, Encoding.UTF8, "application/json");
}
return message;
}
catch
{
message.Dispose();
throw;
}
}
private Uri CreateUri(string path, IReadOnlyDictionary<string, object?>? query)
{
var baseUri = _httpClient.BaseAddress ?? DefaultBaseUri;
if (!baseUri.IsAbsoluteUri ||
baseUri.Scheme != Uri.UriSchemeHttps ||
!string.IsNullOrEmpty(baseUri.UserInfo) ||
!string.IsNullOrEmpty(baseUri.Query) ||
!string.IsNullOrEmpty(baseUri.Fragment))
{
throw new InvalidOperationException(
"HttpClient.BaseAddress must be an absolute HTTPS URL without credentials, a query string, or a fragment.");
}
Uri uri;
if (path[0] != '/' &&
Uri.TryCreate(path, UriKind.Absolute, out var absoluteUri))
{
if (absoluteUri.Scheme != Uri.UriSchemeHttps)
{
throw new ArgumentException(
"Absolute BinaryLane API URLs must use the HTTPS scheme.",
nameof(path));
}
if (!HasSameOrigin(baseUri, absoluteUri))
{
throw new ArgumentException(
"Absolute BinaryLane API URLs must use the configured API origin.",
nameof(path));
}
uri = absoluteUri;
}
else
{
uri = new Uri(baseUri, path.TrimStart('/'));
if (!HasSameOrigin(baseUri, uri))
{
throw new ArgumentException(
"Relative BinaryLane API paths must resolve on the configured API origin.",
nameof(path));
}
}
if (query is null || query.Count == 0)
{
return uri;
}
var builder = new UriBuilder(uri);
var queryBuilder = new StringBuilder();
if (!string.IsNullOrWhiteSpace(builder.Query))
{
queryBuilder.Append(builder.Query.TrimStart('?'));
}
foreach (var pair in query)
{
if (pair.Value is null)
{
continue;
}
if (queryBuilder.Length > 0)
{
queryBuilder.Append('&');
}
queryBuilder.Append(Uri.EscapeDataString(pair.Key));
queryBuilder.Append('=');
queryBuilder.Append(Uri.EscapeDataString(FormatQueryValue(pair.Value)));
}
builder.Query = queryBuilder.ToString();
return builder.Uri;
}
private static bool HasSameOrigin(Uri first, Uri second) =>
string.Equals(first.Scheme, second.Scheme, StringComparison.OrdinalIgnoreCase) &&
string.Equals(first.Host, second.Host, StringComparison.OrdinalIgnoreCase) &&
first.Port == second.Port;
private static string FormatQueryValue(object value)
{
if (value is bool boolean)
{
return boolean ? "true" : "false";
}
if (value is DateTimeOffset dateTimeOffset)
{
return dateTimeOffset.ToString("O", CultureInfo.InvariantCulture);
}
if (value is DateTime dateTime)
{
return dateTime.ToString("O", CultureInfo.InvariantCulture);
}
if (value is IFormattable formattable)
{
return formattable.ToString(null, CultureInfo.InvariantCulture) ?? string.Empty;
}
return value.ToString() ?? string.Empty;
}
private TResponse Deserialize<TResponse>(string body)
{
if (string.IsNullOrWhiteSpace(body))
{
return default!;
}
if (typeof(TResponse) == typeof(JsonElement))
{
using var document = JsonDocument.Parse(body);
return (TResponse)(object)document.RootElement.Clone();
}
var value = JsonSerializer.Deserialize<TResponse>(body, _json.SerializerOptions);
if (value is null && typeof(TResponse).IsValueType)
{
throw new JsonException("BinaryLane returned a JSON null for a non-nullable response value.");
}
return value!;
}
private static ReadOnlyDictionary<string, IReadOnlyList<string>> CollectHeaders(HttpResponseMessage response)
{
var result = new Dictionary<string, IReadOnlyList<string>>(StringComparer.OrdinalIgnoreCase);
foreach (var header in response.Headers)
{
result[header.Key] = new List<string>(header.Value).AsReadOnly();
}
if (response.Content is not null)
{
foreach (var header in response.Content.Headers)
{
result[header.Key] = new List<string>(header.Value).AsReadOnly();
}
}
return new ReadOnlyDictionary<string, IReadOnlyList<string>>(result);
}
private static BinaryLaneApiException CreateException(
HttpStatusCode statusCode,
Uri requestUri,
string body,
IReadOnlyDictionary<string, IReadOnlyList<string>> headers)
{
var problem = ParseProblem(body);
var message = $"BinaryLane API request failed with HTTP {(int)statusCode} ({statusCode}).";
var diagnosticBody = SanitizeAndTruncate(body);
if (statusCode == HttpStatusCode.BadRequest || statusCode == (HttpStatusCode)422)
{
return new BinaryLaneValidationException(message, statusCode, requestUri, problem, diagnosticBody, headers);
}
return statusCode switch
{
HttpStatusCode.Unauthorized => new BinaryLaneUnauthorizedException(message, requestUri, problem, diagnosticBody, headers),
HttpStatusCode.Forbidden => new BinaryLaneForbiddenException(message, requestUri, problem, diagnosticBody, headers),
HttpStatusCode.NotFound => new BinaryLaneNotFoundException(message, requestUri, problem, diagnosticBody, headers),
_ => new BinaryLaneApiException(message, statusCode, requestUri, problem, diagnosticBody, headers),
};
}
private static BinaryLaneApiProblem? ParseProblem(string body)
{
if (string.IsNullOrWhiteSpace(body))
{
return null;
}
try
{
using var document = JsonDocument.Parse(body);
var root = document.RootElement;
if (root.ValueKind != JsonValueKind.Object)
{
return null;
}
return new BinaryLaneApiProblem
{
Type = ReadString(root, "type"),
Title = ReadString(root, "title"),
Detail = ReadString(root, "detail"),
Status = ReadInt32(root, "status"),
};
}
catch (JsonException)
{
return null;
}
}
private static string? ReadString(JsonElement element, string name) =>
element.TryGetProperty(name, out var value) && value.ValueKind == JsonValueKind.String
? value.GetString()
: null;
private static int? ReadInt32(JsonElement element, string name) =>
element.TryGetProperty(name, out var value) && value.TryGetInt32(out var number)
? number
: null;
private static string? SanitizeAndTruncate(string body)
{
if (string.IsNullOrWhiteSpace(body))
{
return null;
}
// Do not expose an unbounded provider response in exception telemetry. Consumers should still
// treat this diagnostic field as potentially sensitive and avoid logging it indiscriminately.
if (body.Length <= MaximumDiagnosticBodyLength)
{
return body;
}
#if NET8_0_OR_GREATER
return string.Concat(body.AsSpan(0, MaximumDiagnosticBodyLength), "…".AsSpan());
#else
return body.Substring(0, MaximumDiagnosticBodyLength) + "…";
#endif
}
private static async Task<string> ReadBodyAsync(HttpContent content, CancellationToken cancellationToken)
{
if (content.Headers.ContentLength is long contentLength && contentLength > MaximumSuccessBodyLength)
{
throw new HttpRequestException(
$"BinaryLane response content exceeded the {MaximumSuccessBodyLength} byte limit.");
}
#if NET8_0_OR_GREATER
await content.LoadIntoBufferAsync(MaximumSuccessBodyLength).ConfigureAwait(false);
return await content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
#else
await content.LoadIntoBufferAsync(MaximumSuccessBodyLength).ConfigureAwait(false);
return await content.ReadAsStringAsync().ConfigureAwait(false);
#endif
}
private static async Task<string> ReadDiagnosticBodyAsync(HttpContent content, CancellationToken cancellationToken)
{
#if NET8_0_OR_GREATER
using var stream = await content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
#else
using var stream = await content.ReadAsStreamAsync().ConfigureAwait(false);
#endif
using var reader = new StreamReader(stream, Encoding.UTF8, true, 4096, leaveOpen: false);
var buffer = new char[MaximumDiagnosticBodyLength + 1];
var count = 0;
while (count < buffer.Length)
{
#if NET8_0_OR_GREATER
var read = await reader
.ReadAsync(buffer.AsMemory(count, buffer.Length - count), cancellationToken)
.ConfigureAwait(false);
#else
var read = await reader
.ReadAsync(buffer, count, buffer.Length - count)
.ConfigureAwait(false);
#endif
if (read == 0)
{
break;
}
count += read;
}
return new string(buffer, 0, count);
}
}
@@ -0,0 +1,24 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using BinaryLane.Api.V2.Models;
namespace BinaryLane.Api.V2.Http;
/// <summary>JSON settings used by the BinaryLane API client.</summary>
public sealed class BinaryLaneJsonSerializerOptions
{
/// <summary>Creates the default BinaryLane JSON settings.</summary>
public BinaryLaneJsonSerializerOptions()
{
SerializerOptions = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
};
SerializerOptions.Converters.Add(new JsonStringEnumConverter());
SerializerOptions.Converters.Add(new ServerActionJsonConverter());
}
/// <summary>The serializer settings used for request and response bodies.</summary>
public JsonSerializerOptions SerializerOptions { get; }
}
@@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
namespace BinaryLane.Api.V2.Http;
/// <summary>Represents one request to the BinaryLane v2 API.</summary>
public sealed class BinaryLaneRequest
{
/// <summary>Initializes a request.</summary>
public BinaryLaneRequest(HttpMethod method, string path)
{
Method = method ?? throw new ArgumentNullException(nameof(method));
if (string.IsNullOrWhiteSpace(path))
{
throw new ArgumentException("An API path is required.", nameof(path));
}
Path = path;
}
/// <summary>HTTP method.</summary>
public HttpMethod Method { get; }
/// <summary>
/// Relative v2 path (for example <c>v2/servers</c>) or an absolute HTTPS URL on the
/// configured API origin. Relative paths are resolved against the configured
/// <see cref="System.Net.Http.HttpClient.BaseAddress"/>. Other origins are rejected so a
/// provider link cannot receive the configured bearer token.
/// </summary>
public string Path { get; }
/// <summary>Optional JSON request body.</summary>
public object? Body { get; set; }
/// <summary>Optional query-string values. Null values are omitted.</summary>
public IReadOnlyDictionary<string, object?>? Query { get; set; }
/// <summary>
/// Optional request headers. Authorization is supplied by the configured token provider and
/// cannot be overridden here.
/// </summary>
public IReadOnlyDictionary<string, string>? Headers { get; set; }
}
@@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.Net;
namespace BinaryLane.Api.V2.Http;
/// <summary>Response metadata and a deserialized BinaryLane API body.</summary>
/// <typeparam name="T">The deserialized response type.</typeparam>
public sealed class BinaryLaneResponse<T>
{
internal BinaryLaneResponse(
T body,
HttpStatusCode statusCode,
Uri requestUri,
IReadOnlyDictionary<string, IReadOnlyList<string>> headers)
{
Body = body;
StatusCode = statusCode;
RequestUri = requestUri;
Headers = headers;
}
/// <summary>Deserialized response body.</summary>
public T Body { get; }
/// <summary>HTTP status code returned by BinaryLane.</summary>
public HttpStatusCode StatusCode { get; }
/// <summary>Final request URI.</summary>
public Uri RequestUri { get; }
/// <summary>Response and content headers.</summary>
public IReadOnlyDictionary<string, IReadOnlyList<string>> Headers { get; }
}
@@ -0,0 +1,57 @@
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace BinaryLane.Api.V2.Http;
/// <summary>
/// Low-level request executor used by resource APIs. It is public so consuming applications can
/// compose support for a newly introduced BinaryLane endpoint before a typed resource is released.
/// </summary>
public interface IBinaryLaneApiExecutor
{
/// <summary>Sends a request and returns response metadata and the deserialized body.</summary>
Task<BinaryLaneResponse<TResponse>> SendAsync<TResponse>(
BinaryLaneRequest request,
CancellationToken cancellationToken = default);
/// <summary>Sends a request which is expected to have no response body.</summary>
Task<BinaryLaneResponse<object?>> SendAsync(
BinaryLaneRequest request,
CancellationToken cancellationToken = default);
/// <summary>Sends a GET request.</summary>
Task<TResponse> GetAsync<TResponse>(
string path,
IReadOnlyDictionary<string, object?>? query = null,
CancellationToken cancellationToken = default);
/// <summary>Sends a POST request.</summary>
Task<TResponse> PostAsync<TResponse>(
string path,
object? body = null,
IReadOnlyDictionary<string, object?>? query = null,
CancellationToken cancellationToken = default);
/// <summary>Sends a PUT request.</summary>
Task<TResponse> PutAsync<TResponse>(
string path,
object? body = null,
IReadOnlyDictionary<string, object?>? query = null,
CancellationToken cancellationToken = default);
/// <summary>Sends a PATCH request.</summary>
Task<TResponse> PatchAsync<TResponse>(
string path,
object? body = null,
IReadOnlyDictionary<string, object?>? query = null,
CancellationToken cancellationToken = default);
/// <summary>Sends a DELETE request.</summary>
Task DeleteAsync(
string path,
object? body = null,
IReadOnlyDictionary<string, object?>? query = null,
CancellationToken cancellationToken = default);
}
+59
View File
@@ -0,0 +1,59 @@
using BinaryLane.Api.V2.Http;
using BinaryLane.Api.V2.Resources;
namespace BinaryLane.Api.V2;
/// <summary>
/// Composable entry point for the BinaryLane v2 API. Each resource is independently represented by
/// an interface so applications can depend on and mock only the portion they use.
/// </summary>
public interface IBinaryLaneClient
{
/// <summary>Low-level executor for a newly introduced endpoint not yet represented by a resource API.</summary>
IBinaryLaneApiExecutor Executor { get; }
/// <summary>Current account information.</summary>
IAccountApi Account { get; }
/// <summary>Global asynchronous actions.</summary>
IActionsApi Actions { get; }
/// <summary>Billing balance and invoices.</summary>
IBillingApi Billing { get; }
/// <summary>Data-transfer usage.</summary>
IDataUsageApi DataUsage { get; }
/// <summary>DNS domains and DNS records.</summary>
IDomainsApi Domains { get; }
/// <summary>Images and image downloads.</summary>
IImagesApi Images { get; }
/// <summary>SSH keys.</summary>
ISshKeysApi SshKeys { get; }
/// <summary>Load balancers, their members, and forwarding rules.</summary>
ILoadBalancersApi LoadBalancers { get; }
/// <summary>Available regions.</summary>
IRegionsApi Regions { get; }
/// <summary>IPv6 reverse nameservers.</summary>
IReverseNamesApi ReverseNames { get; }
/// <summary>Monitoring sample sets.</summary>
ISampleSetsApi SampleSets { get; }
/// <summary>Servers and server-scoped resources.</summary>
IServersApi Servers { get; }
/// <summary>Available server sizes.</summary>
ISizesApi Sizes { get; }
/// <summary>Licensed and operating-system software.</summary>
ISoftwareApi Software { get; }
/// <summary>Private virtual networks.</summary>
IVpcsApi Vpcs { get; }
}
@@ -0,0 +1,31 @@
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace BinaryLane.Api.V2.Models;
/// <summary>
/// Base class for JSON objects returned by the BinaryLane API.
/// </summary>
/// <remarks>
/// BinaryLane's v2 API is a developer preview and may add properties before this
/// SDK is updated. Unknown properties are retained instead of causing a
/// deserialization failure, so applications that need an early provider field can
/// opt in to reading it from <see cref="AdditionalProperties"/>.
/// </remarks>
public abstract class BinaryLaneDto
{
/// <summary>
/// Gets properties supplied by the service that this version of the SDK does
/// not yet model explicitly.
/// </summary>
[JsonExtensionData]
public IDictionary<string, JsonElement>? AdditionalProperties { get; init; }
}
/// <summary>
/// Base class for request payloads accepted by the BinaryLane API.
/// </summary>
public abstract class BinaryLaneRequestModel : BinaryLaneDto
{
}
@@ -0,0 +1,275 @@
namespace BinaryLane.Api.V2.Models;
/// <summary>
/// Known string values in the BinaryLane v2 contract.
/// </summary>
/// <remarks>
/// The API remains a developer preview. Public DTOs use strings rather than
/// closed C# enums so an upstream value added after an SDK release remains
/// readable. These constants make documented values discoverable without
/// removing that forward compatibility.
/// </remarks>
public static class BinaryLaneValues
{
public static class AccountStatus
{
public const string Incomplete = "incomplete";
public const string Active = "active";
public const string Warning = "warning";
public const string Locked = "locked";
}
public static class ActionStatus
{
public const string InProgress = "in-progress";
public const string Completed = "completed";
public const string Errored = "errored";
}
public static class ServerStatus
{
public const string New = "new";
public const string Active = "active";
public const string Archive = "archive";
// This unusual provider value is present in the 0.39.1 OpenAPI contract.
public const string ProviderFalse = "False";
}
public static class ResourceType
{
public const string Server = "server";
public const string LoadBalancer = "load-balancer";
public const string SshKey = "ssh-key";
public const string Vpc = "vpc";
public const string Image = "image";
public const string RegisteredDomainName = "registered-domain-name";
}
public static class ImageType
{
public const string Custom = "custom";
public const string Snapshot = "snapshot";
public const string Backup = "backup";
}
public static class ImageStatus
{
public const string New = "NEW";
public const string Available = "available";
public const string Pending = "pending";
public const string Deleted = "deleted";
}
public static class ImageQueryType
{
public const string Distribution = "distribution";
public const string Backup = "backup";
}
public static class BackupSlot
{
public const string Daily = "daily";
public const string Weekly = "weekly";
public const string Monthly = "monthly";
public const string Temporary = "temporary";
}
public static class BackupReplacementStrategy
{
public const string None = "none";
public const string Specified = "specified";
public const string Oldest = "oldest";
public const string Newest = "newest";
}
public static class NetworkType
{
public const string Private = "private";
public const string Public = "public";
}
public static class DomainRecordType
{
public const string A = "A";
public const string Aaaa = "AAAA";
public const string Caa = "CAA";
public const string Cname = "CNAME";
public const string Mx = "MX";
public const string Ns = "NS";
public const string Soa = "SOA";
public const string Srv = "SRV";
public const string Txt = "TXT";
}
public static class LoadBalancerStatus
{
public const string New = "new";
public const string Active = "active";
public const string Errored = "errored";
}
public static class LoadBalancerRuleProtocol
{
public const string Http = "http";
public const string Https = "https";
}
public static class HealthCheckProtocol
{
public const string Http = "http";
public const string Https = "https";
public const string Both = "both";
}
public static class AdvancedFirewallRuleProtocol
{
public const string All = "all";
public const string Icmp = "icmp";
public const string Tcp = "tcp";
public const string Udp = "udp";
}
public static class AdvancedFirewallRuleAction
{
public const string Drop = "drop";
public const string Accept = "accept";
}
public static class PasswordRecoveryType
{
public const string Manual = "manual";
public const string OfflineClear = "offline-clear";
public const string OfflineChange = "offline-change";
public const string OnlineChange = "online-change";
}
public static class DistributionFeature
{
public const string Ssh = "ssh";
public const string RemoteDesktop = "remote-desktop";
public const string UserData = "user-data";
}
public static class DataInterval
{
public const string FiveMinute = "five-minute";
public const string HalfHour = "half-hour";
public const string FourHour = "four-hour";
public const string Day = "day";
public const string Week = "week";
public const string Month = "month";
}
public static class ThresholdAlertType
{
public const string Cpu = "cpu";
public const string StorageRequests = "storage-requests";
public const string NetworkIncoming = "network-incoming";
public const string NetworkOutgoing = "network-outgoing";
public const string DataTransferUsed = "data-transfer-used";
public const string StorageUsed = "storage-used";
public const string MemoryUsed = "memory-used";
public const string LockedBackupSlots = "locked-backup-slots";
}
public static class PaymentMethod
{
public const string CreditCard = "credit-card";
public const string Paypal = "paypal";
}
public static class TaxCodeType
{
public const string None = "none";
public const string Scalar = "scalar";
}
public static class UserInteractionType
{
public const string ContinueAfterPingFailure = "continue-after-ping-failure";
public const string AllowUncleanPowerOff = "allow-unclean-power-off";
}
public static class AdvancedFeature
{
public const string EmulatedHyperV = "emulated-hyperv";
public const string EmulatedDevices = "emulated-devices";
public const string NestedVirt = "nested-virt";
public const string DriverDisk = "driver-disk";
public const string UnsetUuid = "unset-uuid";
public const string LocalRtc = "local-rtc";
public const string EmulatedTpm = "emulated-tpm";
public const string CloudInit = "cloud-init";
public const string QemuGuestAgent = "qemu-guest-agent";
public const string UefiBoot = "uefi-boot";
}
public static class VideoDevice
{
public const string CirrusLogic = "cirrus-logic";
public const string Standard = "standard";
public const string Virtio = "virtio";
public const string VirtioWide = "virtio-wide";
}
public static class VmMachineType
{
public const string PcI440Fx1Point5 = "pc_i440fx_1point5";
public const string PcI440Fx2Point11 = "pc_i440fx_2point11";
public const string PcI440Fx4Point1 = "pc_i440fx_4point1";
public const string PcI440Fx4Point2 = "pc_i440fx_4point2";
public const string PcI440Fx5Point0 = "pc_i440fx_5point0";
public const string PcI440Fx5Point1 = "pc_i440fx_5point1";
public const string PcI440Fx7Point2 = "pc_i440fx_7point2";
public const string PcI440Fx7Point2Point1 = "pc_i440fx_7point2point1";
public const string PcI440Fx8Point2 = "pc_i440fx_8point2";
}
/// <summary>Discriminator values for <see cref="ServerAction"/> payloads.</summary>
public static class ServerActionType
{
public const string AddDisk = "add_disk";
public const string AttachBackup = "attach_backup";
public const string ChangeAdvancedFeatures = "change_advanced_features";
public const string ChangeAdvancedFirewallRules = "change_advanced_firewall_rules";
public const string ChangeBackupSchedule = "change_backup_schedule";
public const string ChangeIpv6 = "change_ipv6";
public const string ChangeIpv6ReverseNameservers = "change_ipv6_reverse_nameservers";
public const string ChangeKernel = "change_kernel";
public const string ChangeManageOffsiteBackupCopies = "change_manage_offsite_backup_copies";
public const string ChangeNetwork = "change_network";
public const string ChangeOffsiteBackupLocation = "change_offsite_backup_location";
public const string ChangePartner = "change_partner";
public const string ChangePortBlocking = "change_port_blocking";
public const string ChangeRegion = "change_region";
public const string ChangeReverseName = "change_reverse_name";
public const string ChangeSeparatePrivateNetworkInterface = "change_separate_private_network_interface";
public const string ChangeSourceAndDestinationCheck = "change_source_and_destination_check";
public const string ChangeThresholdAlerts = "change_threshold_alerts";
public const string ChangeVpcIpv4 = "change_vpc_ipv4";
public const string CloneUsingBackup = "clone_using_backup";
public const string DeleteDisk = "delete_disk";
public const string DetachBackup = "detach_backup";
public const string DisableBackups = "disable_backups";
public const string DisableSelinux = "disable_selinux";
public const string EnableBackups = "enable_backups";
public const string EnableIpv6 = "enable_ipv6";
public const string IsRunning = "is_running";
public const string PasswordReset = "password_reset";
public const string Ping = "ping";
public const string PowerCycle = "power_cycle";
public const string PowerOff = "power_off";
public const string PowerOn = "power_on";
public const string Reboot = "reboot";
public const string Rebuild = "rebuild";
public const string Rename = "rename";
public const string Resize = "resize";
public const string ResizeDisk = "resize_disk";
public const string Restore = "restore";
public const string Shutdown = "shutdown";
public const string TakeBackup = "take_backup";
public const string Uncancel = "uncancel";
public const string Uptime = "uptime";
}
}
+615
View File
@@ -0,0 +1,615 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace BinaryLane.Api.V2.Models;
/// <summary>Information about the authenticated BinaryLane account.</summary>
public sealed class Account : BinaryLaneDto
{
[JsonPropertyName("email")]
public string Email { get; init; } = string.Empty;
[JsonPropertyName("email_verified")]
public bool EmailVerified { get; init; }
[JsonPropertyName("two_factor_authentication_enabled")]
public bool TwoFactorAuthenticationEnabled { get; init; }
/// <summary>Provider status, for example <c>active</c>.</summary>
[JsonPropertyName("status")]
public string Status { get; init; } = string.Empty;
[JsonPropertyName("tax_code")]
public TaxCode TaxCode { get; init; } = new();
/// <summary>Provider payment-method values, for example <c>credit-card</c>.</summary>
[JsonPropertyName("configured_payment_methods")]
public IReadOnlyList<string> ConfiguredPaymentMethods { get; init; } = Array.Empty<string>();
[JsonPropertyName("additional_ipv4_limit")]
public int AdditionalIpv4Limit { get; init; }
}
/// <summary>A tax code currently applicable to an account or invoice.</summary>
public sealed class TaxCode : BinaryLaneDto
{
[JsonPropertyName("name")]
public string Name { get; init; } = string.Empty;
/// <summary>Provider tax-code type, for example <c>none</c> or <c>scalar</c>.</summary>
[JsonPropertyName("type")]
public string Type { get; init; } = string.Empty;
[JsonPropertyName("fixed_percent")]
public double? FixedPercent { get; init; }
}
/// <summary>A provider action, which may still be in progress.</summary>
public sealed class BinaryLaneAction : BinaryLaneDto
{
[JsonPropertyName("id")]
public long Id { get; init; }
/// <summary>Provider status, for example <c>in-progress</c>, <c>completed</c>, or <c>errored</c>.</summary>
[JsonPropertyName("status")]
public string Status { get; init; } = string.Empty;
[JsonPropertyName("type")]
public string Type { get; init; } = string.Empty;
[JsonPropertyName("started_at")]
public DateTimeOffset StartedAt { get; init; }
[JsonPropertyName("completed_at")]
public DateTimeOffset? CompletedAt { get; init; }
/// <summary>Provider resource type, when the action is associated with one.</summary>
[JsonPropertyName("resource_type")]
public string? ResourceType { get; init; }
[JsonPropertyName("resource_id")]
public long? ResourceId { get; init; }
[JsonPropertyName("region")]
public Region? Region { get; init; }
[JsonPropertyName("region_slug")]
public string? RegionSlug { get; init; }
[JsonPropertyName("title")]
public string Title { get; init; } = string.Empty;
[JsonPropertyName("reason")]
public string Reason { get; init; } = string.Empty;
[JsonPropertyName("progress")]
public ActionProgress Progress { get; init; } = new();
[JsonPropertyName("result_data")]
public string? ResultData { get; init; }
[JsonPropertyName("blocking_invoice_id")]
public long? BlockingInvoiceId { get; init; }
[JsonPropertyName("user_interaction_required")]
public UserInteractionRequired? UserInteractionRequired { get; init; }
}
/// <summary>Progress information for a long-running provider action.</summary>
public sealed class ActionProgress : BinaryLaneDto
{
[JsonPropertyName("current_step_detail")]
public string? CurrentStepDetail { get; init; }
[JsonPropertyName("percent_complete")]
public int PercentComplete { get; init; }
[JsonPropertyName("current_step")]
public string? CurrentStep { get; init; }
[JsonPropertyName("completed_steps")]
public IReadOnlyList<string> CompletedSteps { get; init; } = Array.Empty<string>();
}
/// <summary>A link to an action related to a completed request.</summary>
public sealed class ActionLink : BinaryLaneDto
{
[JsonPropertyName("id")]
public long Id { get; init; }
[JsonPropertyName("rel")]
public string Rel { get; init; } = string.Empty;
[JsonPropertyName("href")]
public string Href { get; init; } = string.Empty;
}
/// <summary>Indicates that an action needs an explicit user decision.</summary>
public sealed class UserInteractionRequired : BinaryLaneDto
{
/// <summary>Provider interaction type, such as <c>continue-after-ping-failure</c>.</summary>
[JsonPropertyName("interaction_type")]
public string InteractionType { get; init; } = string.Empty;
}
/// <summary>A BinaryLane region in which a resource can be created.</summary>
public sealed class Region : BinaryLaneDto
{
[JsonPropertyName("slug")]
public string Slug { get; init; } = string.Empty;
[JsonPropertyName("name")]
public string Name { get; init; } = string.Empty;
[JsonPropertyName("sizes")]
public IReadOnlyList<string> Sizes { get; init; } = Array.Empty<string>();
[JsonPropertyName("available")]
public bool Available { get; init; }
[JsonPropertyName("features")]
public IReadOnlyList<string> Features { get; init; } = Array.Empty<string>();
[JsonPropertyName("name_servers")]
public IReadOnlyList<string> NameServers { get; init; } = Array.Empty<string>();
}
/// <summary>A BinaryLane server.</summary>
public sealed class Server : BinaryLaneDto
{
[JsonPropertyName("id")]
public long Id { get; init; }
[JsonPropertyName("name")]
public string Name { get; init; } = string.Empty;
[JsonPropertyName("memory")]
public int MemoryMegabytes { get; init; }
[JsonPropertyName("vcpus")]
public int Vcpus { get; init; }
[JsonPropertyName("disk")]
public int DiskGigabytes { get; init; }
[JsonPropertyName("vpc_id")]
public long? VpcId { get; init; }
[JsonPropertyName("created_at")]
public DateTimeOffset CreatedAt { get; init; }
/// <summary>Provider server status, for example <c>active</c>.</summary>
[JsonPropertyName("status")]
public string Status { get; init; } = string.Empty;
[JsonPropertyName("backup_ids")]
public IReadOnlyList<long> BackupIds { get; init; } = Array.Empty<long>();
[JsonPropertyName("features")]
public IReadOnlyList<string> Features { get; init; } = Array.Empty<string>();
[JsonPropertyName("region")]
public Region Region { get; init; } = new();
[JsonPropertyName("image")]
public Image Image { get; init; } = new();
[JsonPropertyName("size")]
public Size Size { get; init; } = new();
[JsonPropertyName("size_slug")]
public string SizeSlug { get; init; } = string.Empty;
[JsonPropertyName("selected_size_options")]
public SelectedSizeOptions? SelectedSizeOptions { get; init; }
[JsonPropertyName("networks")]
public ServerNetworks Networks { get; init; } = new();
[JsonPropertyName("kernel")]
public Kernel? Kernel { get; init; }
[JsonPropertyName("next_backup_window")]
public BackupWindow? NextBackupWindow { get; init; }
[JsonPropertyName("disks")]
public IReadOnlyList<Disk> Disks { get; init; } = Array.Empty<Disk>();
[JsonPropertyName("backup_settings")]
public BackupSettings BackupSettings { get; init; } = new();
[JsonPropertyName("cancelled_at")]
public DateTimeOffset? CancelledAt { get; init; }
[JsonPropertyName("failover_ips")]
public IReadOnlyList<string> FailoverIps { get; init; } = Array.Empty<string>();
[JsonPropertyName("host")]
public Host Host { get; init; } = new();
[JsonPropertyName("partner_id")]
public long? PartnerId { get; init; }
[JsonPropertyName("password_change_supported")]
public bool PasswordChangeSupported { get; init; }
[JsonPropertyName("permalink")]
public string? Permalink { get; init; }
[JsonPropertyName("attached_backup")]
public AttachedBackup? AttachedBackup { get; init; }
[JsonPropertyName("advanced_features")]
public AdvancedServerFeatures AdvancedFeatures { get; init; } = new();
[JsonPropertyName("is_under_maintenance")]
public bool? IsUnderMaintenance { get; init; }
}
/// <summary>A server disk.</summary>
public sealed class Disk : BinaryLaneDto
{
[JsonPropertyName("id")]
public long Id { get; init; }
[JsonPropertyName("size_gigabytes")]
public double SizeGigabytes { get; init; }
[JsonPropertyName("description")]
public string? Description { get; init; }
[JsonPropertyName("primary")]
public bool Primary { get; init; }
}
/// <summary>A server network assignment.</summary>
public sealed class ServerNetwork : BinaryLaneDto
{
[JsonPropertyName("ip_address")]
public string IpAddress { get; init; } = string.Empty;
/// <summary>May be represented by the provider as either a number or a string.</summary>
[JsonPropertyName("netmask")]
public object? Netmask { get; init; }
[JsonPropertyName("gateway")]
public string? Gateway { get; init; }
/// <summary>Provider network type, either <c>public</c> or <c>private</c>.</summary>
[JsonPropertyName("type")]
public string Type { get; init; } = string.Empty;
[JsonPropertyName("reverse_name")]
public string? ReverseName { get; init; }
[JsonPropertyName("nat_target")]
public string? NatTarget { get; init; }
}
/// <summary>All IPv4/IPv6 network information associated with a server.</summary>
public sealed class ServerNetworks : BinaryLaneDto
{
[JsonPropertyName("v4")]
public IReadOnlyList<ServerNetwork> V4 { get; init; } = Array.Empty<ServerNetwork>();
[JsonPropertyName("v6")]
public IReadOnlyList<ServerNetwork> V6 { get; init; } = Array.Empty<ServerNetwork>();
[JsonPropertyName("port_blocking")]
public bool PortBlocking { get; init; }
[JsonPropertyName("separate_private_network_interface")]
public bool? SeparatePrivateNetworkInterface { get; init; }
[JsonPropertyName("source_and_destination_check")]
public bool? SourceAndDestinationCheck { get; init; }
[JsonPropertyName("recent_ddos")]
public bool RecentDdos { get; init; }
[JsonPropertyName("ipv6_reverse_nameservers")]
public IReadOnlyList<string>? Ipv6ReverseNameservers { get; init; }
[JsonPropertyName("mac_address")]
public string MacAddress { get; init; } = string.Empty;
}
/// <summary>A BinaryLane image, including operating-system and backup images.</summary>
public sealed class Image : BinaryLaneDto
{
[JsonPropertyName("id")]
public long Id { get; init; }
[JsonPropertyName("name")]
public string Name { get; init; } = string.Empty;
/// <summary>Provider image type, for example <c>custom</c>, <c>snapshot</c>, or <c>backup</c>.</summary>
[JsonPropertyName("type")]
public string Type { get; init; } = string.Empty;
[JsonPropertyName("distribution")]
public string? Distribution { get; init; }
[JsonPropertyName("full_name")]
public string? FullName { get; init; }
[JsonPropertyName("slug")]
public string? Slug { get; init; }
[JsonPropertyName("public")]
public bool IsPublic { get; init; }
[JsonPropertyName("regions")]
public IReadOnlyList<string> Regions { get; init; } = Array.Empty<string>();
[JsonPropertyName("created_at")]
public DateTimeOffset? CreatedAt { get; init; }
[JsonPropertyName("min_disk_size")]
public int MinDiskSizeGigabytes { get; init; }
[JsonPropertyName("size_gigabytes")]
public double SizeGigabytes { get; init; }
[JsonPropertyName("description")]
public string? Description { get; init; }
/// <summary>Provider image status, for example <c>available</c>.</summary>
[JsonPropertyName("status")]
public string Status { get; init; } = string.Empty;
[JsonPropertyName("error_message")]
public string? ErrorMessage { get; init; }
[JsonPropertyName("min_memory_megabytes")]
public int? MinMemoryMegabytes { get; init; }
[JsonPropertyName("distribution_surcharges")]
public DistributionSurcharges? DistributionSurcharges { get; init; }
[JsonPropertyName("distribution_info")]
public DistributionInfo DistributionInfo { get; init; } = new();
[JsonPropertyName("backup_info")]
public BackupInfo? BackupInfo { get; init; }
}
/// <summary>Distribution-specific image installation capabilities.</summary>
public sealed class DistributionInfo : BinaryLaneDto
{
[JsonPropertyName("image_id")]
public long ImageId { get; init; }
/// <summary>Provider password recovery mode.</summary>
[JsonPropertyName("password_recovery")]
public string PasswordRecovery { get; init; } = string.Empty;
[JsonPropertyName("remote_access_user")]
public string? RemoteAccessUser { get; init; }
[JsonPropertyName("features")]
public IReadOnlyList<string> Features { get; init; } = Array.Empty<string>();
}
/// <summary>Additional provider charges for a distribution image.</summary>
public sealed class DistributionSurcharges : BinaryLaneDto
{
[JsonPropertyName("surcharge_base_cost")]
public double? SurchargeBaseCost { get; init; }
[JsonPropertyName("surcharge_per_memory_megabyte")]
public double? SurchargePerMemoryMegabyte { get; init; }
[JsonPropertyName("surcharge_per_memory_max_megabytes")]
public int? SurchargePerMemoryMaxMegabytes { get; init; }
[JsonPropertyName("surcharge_per_vcpu")]
public double? SurchargePerVcpu { get; init; }
[JsonPropertyName("surcharge_min_vcpu")]
public int? SurchargeMinVcpu { get; init; }
}
/// <summary>Backup-specific metadata for an image.</summary>
public sealed class BackupInfo : BinaryLaneDto
{
/// <summary>Provider backup slot, such as <c>daily</c> or <c>temporary</c>.</summary>
[JsonPropertyName("type")]
public string Type { get; init; } = string.Empty;
[JsonPropertyName("server_id")]
public long ServerId { get; init; }
[JsonPropertyName("offsite")]
public bool Offsite { get; init; }
[JsonPropertyName("locked")]
public bool Locked { get; init; }
[JsonPropertyName("iso")]
public bool Iso { get; init; }
[JsonPropertyName("backup_disks")]
public IReadOnlyList<BackupDisk> BackupDisks { get; init; } = Array.Empty<BackupDisk>();
}
/// <summary>A disk contained in a backup image.</summary>
public sealed class BackupDisk : BinaryLaneDto
{
[JsonPropertyName("id")]
public long Id { get; init; }
[JsonPropertyName("size_gigabytes")]
public double SizeGigabytes { get; init; }
[JsonPropertyName("min_disk_size")]
public int MinDiskSizeGigabytes { get; init; }
[JsonPropertyName("description")]
public string? Description { get; init; }
}
/// <summary>A provider server size and its available customisation options.</summary>
public sealed class Size : BinaryLaneDto
{
[JsonPropertyName("slug")]
public string Slug { get; init; } = string.Empty;
[JsonPropertyName("description")]
public string? Description { get; init; }
[JsonPropertyName("cpu_description")]
public string? CpuDescription { get; init; }
[JsonPropertyName("storage_description")]
public string? StorageDescription { get; init; }
[JsonPropertyName("size_type")]
public SizeType SizeType { get; init; } = new();
[JsonPropertyName("available")]
public bool Available { get; init; }
[JsonPropertyName("regions")]
public IReadOnlyList<string> Regions { get; init; } = Array.Empty<string>();
[JsonPropertyName("regions_out_of_stock")]
public IReadOnlyList<string>? RegionsOutOfStock { get; init; }
[JsonPropertyName("price_monthly")]
public double PriceMonthly { get; init; }
[JsonPropertyName("price_hourly")]
public double PriceHourly { get; init; }
[JsonPropertyName("disk")]
public int DiskGigabytes { get; init; }
[JsonPropertyName("memory")]
public int MemoryMegabytes { get; init; }
[JsonPropertyName("transfer")]
public double TransferGigabytes { get; init; }
[JsonPropertyName("excess_transfer_cost_per_gigabyte")]
public double ExcessTransferCostPerGigabyte { get; init; }
[JsonPropertyName("vcpus")]
public int Vcpus { get; init; }
[JsonPropertyName("vcpu_units")]
public string VcpuUnits { get; init; } = string.Empty;
[JsonPropertyName("options")]
public SizeOptions Options { get; init; } = new();
}
/// <summary>The provider's category for a server size.</summary>
public sealed class SizeType : BinaryLaneDto
{
[JsonPropertyName("slug")]
public string Slug { get; init; } = string.Empty;
[JsonPropertyName("name")]
public string Name { get; init; } = string.Empty;
[JsonPropertyName("description")]
public string? Description { get; init; }
}
/// <summary>Configurable bounds and costs associated with a server size.</summary>
public sealed class SizeOptions : BinaryLaneDto
{
[JsonPropertyName("disk_min")]
public int DiskMinGigabytes { get; init; }
[JsonPropertyName("disk_max")]
public int DiskMaxGigabytes { get; init; }
[JsonPropertyName("disk_cost_per_additional_gigabyte")]
public double DiskCostPerAdditionalGigabyte { get; init; }
[JsonPropertyName("restricted_disk_values")]
public IReadOnlyList<int>? RestrictedDiskValues { get; init; }
[JsonPropertyName("memory_max")]
public int MemoryMaxMegabytes { get; init; }
[JsonPropertyName("memory_cost_per_additional_megabyte")]
public double MemoryCostPerAdditionalMegabyte { get; init; }
[JsonPropertyName("transfer_max")]
public double TransferMaxGigabytes { get; init; }
[JsonPropertyName("transfer_cost_per_additional_gigabyte")]
public double TransferCostPerAdditionalGigabyte { get; init; }
[JsonPropertyName("ipv4_addresses_max")]
public int Ipv4AddressesMax { get; init; }
[JsonPropertyName("ipv4_addresses_cost_per_address")]
public double Ipv4AddressesCostPerAddress { get; init; }
[JsonPropertyName("discount_for_no_public_ipv4")]
public double DiscountForNoPublicIpv4 { get; init; }
[JsonPropertyName("daily_backups")]
public int DailyBackups { get; init; }
[JsonPropertyName("weekly_backups")]
public int WeeklyBackups { get; init; }
[JsonPropertyName("monthly_backups")]
public int MonthlyBackups { get; init; }
[JsonPropertyName("backups_cost_per_backup_per_gigabyte")]
public double BackupsCostPerBackupPerGigabyte { get; init; }
[JsonPropertyName("offsite_backups_cost_per_gigabyte")]
public double OffsiteBackupsCostPerGigabyte { get; init; }
[JsonPropertyName("offsite_backup_frequency_cost")]
public OffsiteBackupFrequencyCost OffsiteBackupFrequencyCost { get; init; } = new();
}
/// <summary>The backup frequency costs published for a size.</summary>
public sealed class OffsiteBackupFrequencyCost : BinaryLaneDto
{
[JsonPropertyName("daily_per_gigabyte")]
public double DailyPerGigabyte { get; init; }
[JsonPropertyName("weekly_per_gigabyte")]
public double WeeklyPerGigabyte { get; init; }
[JsonPropertyName("monthly_per_gigabyte")]
public double MonthlyPerGigabyte { get; init; }
}
/// <summary>The options selected for an individual server.</summary>
public sealed class SelectedSizeOptions : BinaryLaneDto
{
[JsonPropertyName("daily_backups")]
public int DailyBackups { get; init; }
[JsonPropertyName("weekly_backups")]
public int WeeklyBackups { get; init; }
[JsonPropertyName("monthly_backups")]
public int MonthlyBackups { get; init; }
[JsonPropertyName("offsite_backups")]
public bool OffsiteBackups { get; init; }
[JsonPropertyName("ipv4_addresses")]
public int Ipv4Addresses { get; init; }
[JsonPropertyName("memory")]
public int MemoryMegabytes { get; init; }
[JsonPropertyName("disk")]
public int DiskGigabytes { get; init; }
[JsonPropertyName("transfer")]
public double TransferGigabytes { get; init; }
}
@@ -0,0 +1,9 @@
#if NETSTANDARD2_0
// The compiler emits this marker for init-only properties. netstandard2.0 does
// not provide it, so the package carries the conventional compatibility shim.
namespace System.Runtime.CompilerServices;
internal static class IsExternalInit
{
}
#endif
+390
View File
@@ -0,0 +1,390 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace BinaryLane.Api.V2.Models;
/// <summary>Options selected while creating or rebuilding a server image.</summary>
public sealed class ImageOptions : BinaryLaneRequestModel
{
[JsonPropertyName("name")]
public string? Name { get; init; }
/// <summary>Each identifier may be either an SSH key ID or a public-key fingerprint.</summary>
[JsonPropertyName("ssh_keys")]
public IReadOnlyList<object>? SshKeys { get; init; }
/// <remarks>Do not log user-data; it can contain secrets.</remarks>
[JsonPropertyName("user_data")]
public string? UserData { get; init; }
/// <remarks>Do not log this value.</remarks>
[JsonPropertyName("password")]
public string? Password { get; init; }
}
/// <summary>Customisable options for a server size.</summary>
public sealed class SizeOptionsRequest : BinaryLaneRequestModel
{
[JsonPropertyName("daily_backups")]
public int? DailyBackups { get; init; }
[JsonPropertyName("weekly_backups")]
public int? WeeklyBackups { get; init; }
[JsonPropertyName("monthly_backups")]
public int? MonthlyBackups { get; init; }
[JsonPropertyName("offsite_backups")]
public bool? OffsiteBackups { get; init; }
[JsonPropertyName("ipv4_addresses")]
public int? Ipv4Addresses { get; init; }
[JsonPropertyName("memory")]
public int? MemoryMegabytes { get; init; }
[JsonPropertyName("disk")]
public int? DiskGigabytes { get; init; }
[JsonPropertyName("transfer")]
public double? TransferGigabytes { get; init; }
}
/// <summary>A requested software licence allocation.</summary>
public sealed class License : BinaryLaneRequestModel
{
[JsonPropertyName("software_id")]
public long SoftwareId { get; init; }
[JsonPropertyName("count")]
public int Count { get; init; }
}
/// <summary>Creates a new BinaryLane server.</summary>
public sealed class CreateServerRequest : BinaryLaneRequestModel
{
[JsonPropertyName("name")]
public string? Name { get; init; }
[JsonPropertyName("backups")]
public bool? Backups { get; init; }
[JsonPropertyName("ipv6")]
public bool? Ipv6 { get; init; }
[JsonPropertyName("size")]
public string Size { get; init; } = string.Empty;
/// <summary>An image ID or provider image slug.</summary>
[JsonPropertyName("image")]
public object Image { get; init; } = string.Empty;
[JsonPropertyName("region")]
public string Region { get; init; } = string.Empty;
[JsonPropertyName("vpc_id")]
public long? VpcId { get; init; }
[JsonPropertyName("vpc_ipv4_address")]
public string? VpcIpv4Address { get; init; }
/// <summary>Each entry may be an SSH key ID or fingerprint.</summary>
[JsonPropertyName("ssh_keys")]
public IReadOnlyList<object>? SshKeys { get; init; }
[JsonPropertyName("options")]
public SizeOptionsRequest? Options { get; init; }
[JsonPropertyName("licenses")]
public IReadOnlyList<License>? Licenses { get; init; }
/// <remarks>Do not log user-data; it may contain secrets.</remarks>
[JsonPropertyName("user_data")]
public string? UserData { get; init; }
[JsonPropertyName("port_blocking")]
public bool? PortBlocking { get; init; }
[JsonPropertyName("separate_private_network_interface")]
public bool? SeparatePrivateNetworkInterface { get; init; }
/// <remarks>Do not log this value.</remarks>
[JsonPropertyName("password")]
public string? Password { get; init; }
}
/// <summary>Creates a new VPC.</summary>
public sealed class CreateVpcRequest : BinaryLaneRequestModel
{
[JsonPropertyName("name")]
public string Name { get; init; } = string.Empty;
[JsonPropertyName("route_entries")]
public IReadOnlyList<RouteEntryRequest>? RouteEntries { get; init; }
[JsonPropertyName("ip_range")]
public string? IpRange { get; init; }
}
/// <summary>Replaces VPC configuration. Omitted properties are cleared by the provider.</summary>
public sealed class UpdateVpcRequest : BinaryLaneRequestModel
{
[JsonPropertyName("name")]
public string Name { get; init; } = string.Empty;
[JsonPropertyName("route_entries")]
public IReadOnlyList<RouteEntryRequest>? RouteEntries { get; init; }
}
/// <summary>Partially updates VPC configuration.</summary>
public sealed class PatchVpcRequest : BinaryLaneRequestModel
{
[JsonPropertyName("name")]
public string? Name { get; init; }
[JsonPropertyName("route_entries")]
public IReadOnlyList<RouteEntryRequest>? RouteEntries { get; init; }
}
/// <summary>A VPC route entry supplied in a create or update request.</summary>
public sealed class RouteEntryRequest : BinaryLaneRequestModel
{
[JsonPropertyName("router")]
public string Router { get; init; } = string.Empty;
[JsonPropertyName("destination")]
public string Destination { get; init; } = string.Empty;
[JsonPropertyName("description")]
public string? Description { get; init; }
}
/// <summary>Creates a new load balancer.</summary>
public sealed class CreateLoadBalancerRequest : BinaryLaneRequestModel
{
[JsonPropertyName("name")]
public string Name { get; init; } = string.Empty;
[JsonPropertyName("forwarding_rules")]
public IReadOnlyList<ForwardingRuleRequest>? ForwardingRules { get; init; }
[JsonPropertyName("health_check")]
public HealthCheckRequest? HealthCheck { get; init; }
[JsonPropertyName("server_ids")]
public IReadOnlyList<long>? ServerIds { get; init; }
[JsonPropertyName("region")]
public string? Region { get; init; }
}
/// <summary>Updates a load balancer.</summary>
public sealed class UpdateLoadBalancerRequest : BinaryLaneRequestModel
{
[JsonPropertyName("name")]
public string Name { get; init; } = string.Empty;
[JsonPropertyName("forwarding_rules")]
public IReadOnlyList<ForwardingRuleRequest>? ForwardingRules { get; init; }
[JsonPropertyName("health_check")]
public HealthCheckRequest? HealthCheck { get; init; }
[JsonPropertyName("server_ids")]
public IReadOnlyList<long>? ServerIds { get; init; }
}
/// <summary>A load-balancer forwarding rule supplied in a request.</summary>
public sealed class ForwardingRuleRequest : BinaryLaneRequestModel
{
/// <summary>Either <c>http</c> or <c>https</c>.</summary>
[JsonPropertyName("entry_protocol")]
public string EntryProtocol { get; init; } = string.Empty;
}
/// <summary>A collection of load-balancer forwarding rules.</summary>
public sealed class ForwardingRulesRequest : BinaryLaneRequestModel
{
[JsonPropertyName("forwarding_rules")]
public IReadOnlyList<ForwardingRuleRequest> ForwardingRules { get; init; } = Array.Empty<ForwardingRuleRequest>();
}
/// <summary>A load-balancer health check supplied in a request.</summary>
public sealed class HealthCheckRequest : BinaryLaneRequestModel
{
/// <summary>One of <c>http</c>, <c>https</c>, or <c>both</c>.</summary>
[JsonPropertyName("protocol")]
public string? Protocol { get; init; }
[JsonPropertyName("path")]
public string? Path { get; init; }
}
/// <summary>A collection of server IDs supplied to a load-balancer operation.</summary>
public sealed class ServerIdsRequest : BinaryLaneRequestModel
{
[JsonPropertyName("server_ids")]
public IReadOnlyList<long> ServerIds { get; init; } = Array.Empty<long>();
}
/// <summary>Creates a DNS domain.</summary>
public sealed class DomainRequest : BinaryLaneRequestModel
{
[JsonPropertyName("name")]
public string Name { get; init; } = string.Empty;
[JsonPropertyName("ip_address")]
public string? IpAddress { get; init; }
}
/// <summary>Requests refresh of the nameserver cache for domains.</summary>
public sealed class DomainRefreshRequest : BinaryLaneRequestModel
{
[JsonPropertyName("domain_names")]
public IReadOnlyList<string> DomainNames { get; init; } = Array.Empty<string>();
}
/// <summary>Creates a DNS record.</summary>
public sealed class DomainRecordRequest : BinaryLaneRequestModel
{
[JsonPropertyName("type")]
public string Type { get; init; } = string.Empty;
[JsonPropertyName("name")]
public string Name { get; init; } = string.Empty;
[JsonPropertyName("data")]
public string Data { get; init; } = string.Empty;
[JsonPropertyName("priority")]
public int? Priority { get; init; }
[JsonPropertyName("port")]
public int? Port { get; init; }
[JsonPropertyName("ttl")]
public int? Ttl { get; init; }
[JsonPropertyName("weight")]
public int? Weight { get; init; }
[JsonPropertyName("flags")]
public int? Flags { get; init; }
[JsonPropertyName("tag")]
public string? Tag { get; init; }
}
/// <summary>Updates only the supplied parts of a DNS record.</summary>
public sealed class UpdateDomainRecordRequest : BinaryLaneRequestModel
{
[JsonPropertyName("type")]
public string? Type { get; init; }
[JsonPropertyName("name")]
public string? Name { get; init; }
[JsonPropertyName("data")]
public string? Data { get; init; }
[JsonPropertyName("priority")]
public int? Priority { get; init; }
[JsonPropertyName("port")]
public int? Port { get; init; }
[JsonPropertyName("ttl")]
public int? Ttl { get; init; }
[JsonPropertyName("weight")]
public int? Weight { get; init; }
[JsonPropertyName("flags")]
public int? Flags { get; init; }
[JsonPropertyName("tag")]
public string? Tag { get; init; }
}
/// <summary>Creates an SSH key.</summary>
public sealed class SshKeyRequest : BinaryLaneRequestModel
{
[JsonPropertyName("public_key")]
public string PublicKey { get; init; } = string.Empty;
[JsonPropertyName("name")]
public string Name { get; init; } = string.Empty;
[JsonPropertyName("default")]
public bool? IsDefault { get; init; }
}
/// <summary>Updates an SSH key.</summary>
public sealed class UpdateSshKeyRequest : BinaryLaneRequestModel
{
[JsonPropertyName("name")]
public string Name { get; init; } = string.Empty;
[JsonPropertyName("default")]
public bool? IsDefault { get; init; }
}
/// <summary>Changes mutable metadata on an image.</summary>
public sealed class ImageRequest : BinaryLaneRequestModel
{
[JsonPropertyName("name")]
public string? Name { get; init; }
[JsonPropertyName("locked")]
public bool? Locked { get; init; }
}
/// <summary>Uploads an image into a backup slot.</summary>
public sealed class UploadImageRequest : BinaryLaneRequestModel
{
/// <summary>Provider backup slot, required unless the replacement strategy is <c>specified</c>.</summary>
[JsonPropertyName("backup_type")]
public string? BackupType { get; init; }
/// <summary>Provider replacement strategy: <c>none</c>, <c>specified</c>, <c>oldest</c>, or <c>newest</c>.</summary>
[JsonPropertyName("replacement_strategy")]
public string ReplacementStrategy { get; init; } = string.Empty;
[JsonPropertyName("backup_id_to_replace")]
public long? BackupIdToReplace { get; init; }
[JsonPropertyName("label")]
public string? Label { get; init; }
[JsonPropertyName("url")]
public string Url { get; init; } = string.Empty;
}
/// <summary>Updates the IPv6 reverse nameservers configured for the account.</summary>
public sealed class ReverseNameserversRequest : BinaryLaneRequestModel
{
[JsonPropertyName("reverse_nameservers")]
public IReadOnlyList<string> ReverseNameservers { get; init; } = Array.Empty<string>();
}
/// <summary>Allows or declines the interaction requested by an action.</summary>
public sealed class ProceedRequest : BinaryLaneRequestModel
{
[JsonPropertyName("proceed")]
public bool Proceed { get; init; }
}
/// <summary>Updates threshold-alert configuration.</summary>
public sealed class ThresholdAlertRequest : BinaryLaneRequestModel
{
/// <summary>Provider alert type.</summary>
[JsonPropertyName("alert_type")]
public string AlertType { get; init; } = string.Empty;
[JsonPropertyName("enabled")]
public bool? Enabled { get; init; }
[JsonPropertyName("value")]
public int? Value { get; init; }
}
+653
View File
@@ -0,0 +1,653 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace BinaryLane.Api.V2.Models;
/// <summary>The current billing balance for the authenticated account.</summary>
public sealed class Balance : BinaryLaneDto
{
[JsonPropertyName("unbilled_total")]
public double UnbilledTotal { get; init; }
[JsonPropertyName("available_credit")]
public double AvailableCredit { get; init; }
[JsonPropertyName("charges")]
public IReadOnlyList<ChargeInformation> Charges { get; init; } = Array.Empty<ChargeInformation>();
[JsonPropertyName("generated_at")]
public DateTimeOffset? GeneratedAt { get; init; }
}
/// <summary>A charge included in a balance response.</summary>
public sealed class ChargeInformation : BinaryLaneDto
{
[JsonPropertyName("created")]
public DateTimeOffset Created { get; init; }
[JsonPropertyName("description")]
public string Description { get; init; } = string.Empty;
[JsonPropertyName("total")]
public double Total { get; init; }
[JsonPropertyName("ongoing")]
public bool Ongoing { get; init; }
}
/// <summary>A BinaryLane invoice.</summary>
public sealed class Invoice : BinaryLaneDto
{
[JsonPropertyName("invoice_id")]
public long InvoiceId { get; init; }
[JsonPropertyName("reference")]
public string? Reference { get; init; }
[JsonPropertyName("invoice_number")]
public string InvoiceNumber { get; init; } = string.Empty;
[JsonPropertyName("amount")]
public double Amount { get; init; }
[JsonPropertyName("tax_code")]
public TaxCode TaxCode { get; init; } = new();
[JsonPropertyName("tax")]
public double Tax { get; init; }
[JsonPropertyName("created")]
public DateTimeOffset Created { get; init; }
[JsonPropertyName("date_due")]
public DateTimeOffset DateDue { get; init; }
[JsonPropertyName("date_overdue")]
public DateTimeOffset DateOverdue { get; init; }
[JsonPropertyName("paid")]
public bool Paid { get; init; }
[JsonPropertyName("refunded")]
public bool Refunded { get; init; }
[JsonPropertyName("payment_failure_count")]
public int? PaymentFailureCount { get; init; }
[JsonPropertyName("invoice_items")]
public IReadOnlyList<InvoiceLineItem> InvoiceItems { get; init; } = Array.Empty<InvoiceLineItem>();
[JsonPropertyName("invoice_download_url")]
public string? InvoiceDownloadUrl { get; init; }
[JsonPropertyName("invoice_view_url")]
public string? InvoiceViewUrl { get; init; }
}
/// <summary>A line item in a BinaryLane invoice.</summary>
public sealed class InvoiceLineItem : BinaryLaneDto
{
[JsonPropertyName("name")]
public string Name { get; init; } = string.Empty;
[JsonPropertyName("amount")]
public double Amount { get; init; }
[JsonPropertyName("amount_includes_tax")]
public bool AmountIncludesTax { get; init; }
}
/// <summary>Current data-transfer use for a server.</summary>
public sealed class DataUsage : BinaryLaneDto
{
[JsonPropertyName("server_id")]
public long ServerId { get; init; }
[JsonPropertyName("expires")]
public DateTimeOffset Expires { get; init; }
[JsonPropertyName("transfer_gigabytes")]
public long TransferGigabytes { get; init; }
[JsonPropertyName("current_transfer_usage_gigabytes")]
public double CurrentTransferUsageGigabytes { get; init; }
[JsonPropertyName("transfer_period_end")]
public DateTimeOffset TransferPeriodEnd { get; init; }
}
/// <summary>A DNS domain managed through the BinaryLane API.</summary>
public sealed class Domain : BinaryLaneDto
{
[JsonPropertyName("id")]
public long Id { get; init; }
[JsonPropertyName("name")]
public string Name { get; init; } = string.Empty;
[JsonPropertyName("current_nameservers")]
public IReadOnlyList<string> CurrentNameservers { get; init; } = Array.Empty<string>();
[JsonPropertyName("ttl")]
public int? Ttl { get; init; }
[JsonPropertyName("zone_file")]
public string ZoneFile { get; init; } = string.Empty;
}
/// <summary>A DNS record in a BinaryLane-managed domain.</summary>
public sealed class DomainRecord : BinaryLaneDto
{
[JsonPropertyName("id")]
public long Id { get; init; }
/// <summary>Provider DNS record type, such as <c>A</c>, <c>AAAA</c>, or <c>TXT</c>.</summary>
[JsonPropertyName("type")]
public string Type { get; init; } = string.Empty;
[JsonPropertyName("name")]
public string Name { get; init; } = string.Empty;
[JsonPropertyName("data")]
public string? Data { get; init; }
[JsonPropertyName("priority")]
public int? Priority { get; init; }
[JsonPropertyName("port")]
public int? Port { get; init; }
[JsonPropertyName("ttl")]
public int Ttl { get; init; }
[JsonPropertyName("weight")]
public int? Weight { get; init; }
[JsonPropertyName("flags")]
public int? Flags { get; init; }
[JsonPropertyName("tag")]
public string? Tag { get; init; }
}
/// <summary>A temporary download link for an image.</summary>
public sealed class ImageDownload : BinaryLaneDto
{
[JsonPropertyName("id")]
public long Id { get; init; }
[JsonPropertyName("expiry")]
public DateTimeOffset Expiry { get; init; }
[JsonPropertyName("disks")]
public IReadOnlyList<ImageDiskDownload> Disks { get; init; } = Array.Empty<ImageDiskDownload>();
}
/// <summary>Download URLs for one disk in an image download.</summary>
public sealed class ImageDiskDownload : BinaryLaneDto
{
[JsonPropertyName("id")]
public long Id { get; init; }
[JsonPropertyName("compressed_url")]
public string CompressedUrl { get; init; } = string.Empty;
[JsonPropertyName("raw_url")]
public string RawUrl { get; init; } = string.Empty;
}
/// <summary>An SSH public key owned by the authenticated account.</summary>
public sealed class SshKey : BinaryLaneDto
{
[JsonPropertyName("id")]
public long Id { get; init; }
[JsonPropertyName("fingerprint")]
public string Fingerprint { get; init; } = string.Empty;
[JsonPropertyName("public_key")]
public string PublicKey { get; init; } = string.Empty;
[JsonPropertyName("name")]
public string? Name { get; init; }
[JsonPropertyName("default")]
public bool IsDefault { get; init; }
}
/// <summary>A BinaryLane load balancer.</summary>
public sealed class LoadBalancer : BinaryLaneDto
{
[JsonPropertyName("id")]
public long Id { get; init; }
[JsonPropertyName("name")]
public string Name { get; init; } = string.Empty;
[JsonPropertyName("ip")]
public string Ip { get; init; } = string.Empty;
/// <summary>Provider load balancer status, such as <c>active</c>.</summary>
[JsonPropertyName("status")]
public string Status { get; init; } = string.Empty;
[JsonPropertyName("created_at")]
public DateTimeOffset CreatedAt { get; init; }
[JsonPropertyName("forwarding_rules")]
public IReadOnlyList<ForwardingRule> ForwardingRules { get; init; } = Array.Empty<ForwardingRule>();
[JsonPropertyName("health_check")]
public HealthCheck HealthCheck { get; init; } = new();
[JsonPropertyName("region")]
public Region? Region { get; init; }
[JsonPropertyName("server_ids")]
public IReadOnlyList<long> ServerIds { get; init; } = Array.Empty<long>();
}
/// <summary>A load-balancer forwarding rule.</summary>
public sealed class ForwardingRule : BinaryLaneDto
{
/// <summary>Provider rule protocol, either <c>http</c> or <c>https</c>.</summary>
[JsonPropertyName("entry_protocol")]
public string EntryProtocol { get; init; } = string.Empty;
}
/// <summary>A load-balancer health check.</summary>
public sealed class HealthCheck : BinaryLaneDto
{
/// <summary>Provider health check protocol, such as <c>http</c>, <c>https</c>, or <c>both</c>.</summary>
[JsonPropertyName("protocol")]
public string Protocol { get; init; } = string.Empty;
[JsonPropertyName("path")]
public string Path { get; init; } = string.Empty;
}
/// <summary>A load-balancer configuration option available to the account.</summary>
public sealed class LoadBalancerAvailabilityOption : BinaryLaneDto
{
[JsonPropertyName("regions")]
public IReadOnlyList<string>? Regions { get; init; }
[JsonPropertyName("anycast")]
public bool Anycast { get; init; }
[JsonPropertyName("price_monthly")]
public double PriceMonthly { get; init; }
[JsonPropertyName("price_hourly")]
public double PriceHourly { get; init; }
}
/// <summary>A virtual private cloud.</summary>
public sealed class Vpc : BinaryLaneDto
{
[JsonPropertyName("id")]
public long Id { get; init; }
[JsonPropertyName("name")]
public string Name { get; init; } = string.Empty;
[JsonPropertyName("ip_range")]
public string IpRange { get; init; } = string.Empty;
[JsonPropertyName("route_entries")]
public IReadOnlyList<RouteEntry> RouteEntries { get; init; } = Array.Empty<RouteEntry>();
}
/// <summary>A route within a VPC.</summary>
public sealed class RouteEntry : BinaryLaneDto
{
[JsonPropertyName("router")]
public string Router { get; init; } = string.Empty;
[JsonPropertyName("destination")]
public string Destination { get; init; } = string.Empty;
[JsonPropertyName("description")]
public string? Description { get; init; }
}
/// <summary>A resource connected to a VPC.</summary>
public sealed class VpcMember : BinaryLaneDto
{
[JsonPropertyName("name")]
public string Name { get; init; } = string.Empty;
/// <summary>Provider resource type.</summary>
[JsonPropertyName("resource_type")]
public string ResourceType { get; init; } = string.Empty;
/// <remarks>The provider documents this as a string, unlike action resource identifiers.</remarks>
[JsonPropertyName("resource_id")]
public string ResourceId { get; init; } = string.Empty;
[JsonPropertyName("created_at")]
public DateTimeOffset? CreatedAt { get; init; }
}
/// <summary>A scheduled backup window.</summary>
public sealed class BackupWindow : BinaryLaneDto
{
[JsonPropertyName("start")]
public DateTimeOffset Start { get; init; }
[JsonPropertyName("end")]
public DateTimeOffset End { get; init; }
}
/// <summary>A server's configured backup schedule.</summary>
public sealed class BackupSettings : BinaryLaneDto
{
[JsonPropertyName("backup_hour_of_day")]
public int BackupHourOfDay { get; init; }
[JsonPropertyName("backup_day_of_week")]
public int BackupDayOfWeek { get; init; }
[JsonPropertyName("backup_day_of_month")]
public int BackupDayOfMonth { get; init; }
[JsonPropertyName("offsite_backup_settings")]
public OffsiteBackupSettings? OffsiteBackupSettings { get; init; }
}
/// <summary>Offsite-backup configuration for a server.</summary>
public sealed class OffsiteBackupSettings : BinaryLaneDto
{
[JsonPropertyName("use_custom_backup_location")]
public bool UseCustomBackupLocation { get; init; }
[JsonPropertyName("offsite_backup_location")]
public string? OffsiteBackupLocation { get; init; }
[JsonPropertyName("manage_offsite_copies")]
public bool? ManageOffsiteCopies { get; init; }
}
/// <summary>The host backing a server.</summary>
public sealed class Host : BinaryLaneDto
{
[JsonPropertyName("display_name")]
public string DisplayName { get; init; } = string.Empty;
[JsonPropertyName("uptime_ms")]
public long? UptimeMilliseconds { get; init; }
[JsonPropertyName("status_page")]
public string? StatusPage { get; init; }
}
/// <summary>Metadata about a backup currently attached to a server.</summary>
public sealed class AttachedBackup : BinaryLaneDto
{
[JsonPropertyName("id")]
public long Id { get; init; }
[JsonPropertyName("disk_identifiers")]
public IReadOnlyList<string> DiskIdentifiers { get; init; } = Array.Empty<string>();
[JsonPropertyName("attached_at")]
public DateTimeOffset? AttachedAt { get; init; }
[JsonPropertyName("attachment_expires")]
public DateTimeOffset? AttachmentExpires { get; init; }
}
/// <summary>Advanced virtual-machine features configured for a server.</summary>
public sealed class AdvancedServerFeatures : BinaryLaneDto
{
[JsonPropertyName("processor_model")]
public long? ProcessorModel { get; init; }
/// <summary>Provider virtual machine type.</summary>
[JsonPropertyName("machine_type")]
public string? MachineType { get; init; }
/// <summary>Provider virtual video device.</summary>
[JsonPropertyName("video_device")]
public string VideoDevice { get; init; } = string.Empty;
/// <summary>Provider advanced-feature values.</summary>
[JsonPropertyName("enabled_advanced_features")]
public IReadOnlyList<string> EnabledAdvancedFeatures { get; init; } = Array.Empty<string>();
}
/// <summary>Advanced server features currently available to the account.</summary>
public sealed class AvailableAdvancedServerFeatures : BinaryLaneDto
{
[JsonPropertyName("processor_models")]
public IReadOnlyList<ProcessorModel> ProcessorModels { get; init; } = Array.Empty<ProcessorModel>();
[JsonPropertyName("machine_types")]
public IReadOnlyList<string> MachineTypes { get; init; } = Array.Empty<string>();
[JsonPropertyName("advanced_features")]
public IReadOnlyList<string> AdvancedFeatures { get; init; } = Array.Empty<string>();
}
/// <summary>An available processor model.</summary>
public sealed class ProcessorModel : BinaryLaneDto
{
[JsonPropertyName("id")]
public long Id { get; init; }
[JsonPropertyName("name")]
public string Name { get; init; } = string.Empty;
[JsonPropertyName("description")]
public string? Description { get; init; }
}
/// <summary>An advanced firewall rule applied to a server.</summary>
public sealed class AdvancedFirewallRule : BinaryLaneDto
{
[JsonPropertyName("source_addresses")]
public IReadOnlyList<string> SourceAddresses { get; init; } = Array.Empty<string>();
[JsonPropertyName("destination_addresses")]
public IReadOnlyList<string> DestinationAddresses { get; init; } = Array.Empty<string>();
[JsonPropertyName("destination_ports")]
public IReadOnlyList<string>? DestinationPorts { get; init; }
/// <summary>Provider protocol value, such as <c>tcp</c>.</summary>
[JsonPropertyName("protocol")]
public string Protocol { get; init; } = string.Empty;
/// <summary>Provider action value, such as <c>accept</c>.</summary>
[JsonPropertyName("action")]
public string Action { get; init; } = string.Empty;
[JsonPropertyName("description")]
public string? Description { get; init; }
}
/// <summary>An available server kernel.</summary>
public sealed class Kernel : BinaryLaneDto
{
[JsonPropertyName("id")]
public long Id { get; init; }
[JsonPropertyName("name")]
public string? Name { get; init; }
[JsonPropertyName("version")]
public string? Version { get; init; }
}
/// <summary>A configured server threshold alert.</summary>
public sealed class ThresholdAlert : BinaryLaneDto
{
/// <summary>Provider alert type.</summary>
[JsonPropertyName("alert_type")]
public string AlertType { get; init; } = string.Empty;
[JsonPropertyName("name")]
public string Name { get; init; } = string.Empty;
[JsonPropertyName("unit")]
public string Unit { get; init; } = string.Empty;
[JsonPropertyName("description")]
public string Description { get; init; } = string.Empty;
[JsonPropertyName("enabled")]
public bool Enabled { get; init; }
[JsonPropertyName("value")]
public int Value { get; init; }
[JsonPropertyName("current_value")]
public int? CurrentValue { get; init; }
[JsonPropertyName("last_raised")]
public DateTimeOffset? LastRaised { get; init; }
[JsonPropertyName("last_cleared")]
public DateTimeOffset? LastCleared { get; init; }
}
/// <summary>Software licensed on a server.</summary>
public sealed class LicensedSoftware : BinaryLaneDto
{
[JsonPropertyName("software")]
public Software Software { get; init; } = new();
[JsonPropertyName("licence_count")]
public int LicenceCount { get; init; }
[JsonPropertyName("incompatible")]
public bool Incompatible { get; init; }
}
/// <summary>A software product that can be licensed on BinaryLane servers.</summary>
public sealed class Software : BinaryLaneDto
{
[JsonPropertyName("id")]
public long Id { get; init; }
[JsonPropertyName("enabled")]
public bool Enabled { get; init; }
[JsonPropertyName("name")]
public string Name { get; init; } = string.Empty;
[JsonPropertyName("description")]
public string Description { get; init; } = string.Empty;
[JsonPropertyName("cost_per_licence_per_month")]
public double CostPerLicencePerMonth { get; init; }
[JsonPropertyName("minimum_licence_count")]
public int MinimumLicenceCount { get; init; }
[JsonPropertyName("maximum_licence_count")]
public int MaximumLicenceCount { get; init; }
[JsonPropertyName("licence_step_count")]
public int LicenceStepCount { get; init; }
[JsonPropertyName("group")]
public string? Group { get; init; }
[JsonPropertyName("supported_operating_systems")]
public IReadOnlyList<string> SupportedOperatingSystems { get; init; } = Array.Empty<string>();
}
/// <summary>A browser console session for a server.</summary>
public sealed class ServerConsole : BinaryLaneDto
{
[JsonPropertyName("iframe")]
public string Iframe { get; init; } = string.Empty;
[JsonPropertyName("browser")]
public string Browser { get; init; } = string.Empty;
[JsonPropertyName("width")]
public int Width { get; init; }
[JsonPropertyName("height")]
public int Height { get; init; }
[JsonPropertyName("expiry")]
public DateTimeOffset Expiry { get; init; }
}
/// <summary>Server user-data retained by BinaryLane.</summary>
public sealed class UserData : BinaryLaneDto
{
[JsonPropertyName("user_data")]
public string? Value { get; init; }
}
/// <summary>A sample interval for server monitoring data.</summary>
public sealed class Period : BinaryLaneDto
{
[JsonPropertyName("start")]
public DateTimeOffset Start { get; init; }
[JsonPropertyName("end")]
public DateTimeOffset End { get; init; }
/// <summary>Provider interval, such as <c>five-minute</c> or <c>day</c>.</summary>
[JsonPropertyName("data_interval")]
public string DataInterval { get; init; } = string.Empty;
}
/// <summary>One monitoring aggregate sample.</summary>
public sealed class Sample : BinaryLaneDto
{
[JsonPropertyName("cpu_usage_percent")]
public double CpuUsagePercent { get; init; }
[JsonPropertyName("cpu_usage_detailed")]
public IReadOnlyList<double> CpuUsageDetailed { get; init; } = Array.Empty<double>();
[JsonPropertyName("memory_usage_bytes")]
public double MemoryUsageBytes { get; init; }
[JsonPropertyName("network_incoming_kbps")]
public double NetworkIncomingKbps { get; init; }
[JsonPropertyName("network_outgoing_kbps")]
public double NetworkOutgoingKbps { get; init; }
[JsonPropertyName("storage_usage_megabytes")]
public double StorageUsageMegabytes { get; init; }
[JsonPropertyName("storage_read_kbps")]
public double StorageReadKbps { get; init; }
[JsonPropertyName("storage_write_kbps")]
public double StorageWriteKbps { get; init; }
[JsonPropertyName("storage_read_requests_per_second")]
public double StorageReadRequestsPerSecond { get; init; }
[JsonPropertyName("storage_write_requests_per_second")]
public double StorageWriteRequestsPerSecond { get; init; }
}
/// <summary>A set of monitoring samples for a server.</summary>
public sealed class SampleSet : BinaryLaneDto
{
[JsonPropertyName("server_id")]
public long ServerId { get; init; }
[JsonPropertyName("period")]
public Period Period { get; init; } = new();
[JsonPropertyName("average")]
public Sample Average { get; init; } = new();
[JsonPropertyName("maximum_memory_megabytes")]
public double MaximumMemoryMegabytes { get; init; }
[JsonPropertyName("maximum_storage_gigabytes")]
public double MaximumStorageGigabytes { get; init; }
}
+472
View File
@@ -0,0 +1,472 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace BinaryLane.Api.V2.Models;
/// <summary>Pagination metadata returned by BinaryLane list endpoints.</summary>
public sealed class PageMeta : BinaryLaneDto
{
[JsonPropertyName("total")]
public int Total { get; init; }
}
/// <summary>Pagination links returned by BinaryLane list endpoints.</summary>
public sealed class PageLinks : BinaryLaneDto
{
[JsonPropertyName("pages")]
public PageNavigation Pages { get; init; } = new();
}
/// <summary>URLs for neighbouring pages in a BinaryLane list response.</summary>
public sealed class PageNavigation : BinaryLaneDto
{
[JsonPropertyName("first")]
public string? First { get; init; }
[JsonPropertyName("prev")]
public string? Previous { get; init; }
[JsonPropertyName("next")]
public string? Next { get; init; }
[JsonPropertyName("last")]
public string? Last { get; init; }
}
/// <summary>An RFC 7807-style problem returned by BinaryLane.</summary>
public class ProblemDetails : BinaryLaneDto
{
[JsonPropertyName("type")]
public string? Type { get; init; }
[JsonPropertyName("title")]
public string? Title { get; init; }
[JsonPropertyName("status")]
public int? Status { get; init; }
[JsonPropertyName("detail")]
public string? Detail { get; init; }
[JsonPropertyName("instance")]
public string? Instance { get; init; }
}
/// <summary>A validation problem returned by BinaryLane.</summary>
public sealed class ValidationProblemDetails : ProblemDetails
{
[JsonPropertyName("errors")]
public IReadOnlyDictionary<string, IReadOnlyList<string>>? Errors { get; init; }
}
public sealed class AccountResponse : BinaryLaneDto
{
[JsonPropertyName("account")]
public Account Account { get; init; } = new();
}
public sealed class ActionResponse : BinaryLaneDto
{
[JsonPropertyName("action")]
public BinaryLaneAction Action { get; init; } = new();
}
public sealed class ActionsLinks : BinaryLaneDto
{
[JsonPropertyName("actions")]
public IReadOnlyList<ActionLink> Actions { get; init; } = Array.Empty<ActionLink>();
}
public sealed class ActionsResponse : BinaryLaneDto
{
[JsonPropertyName("meta")]
public PageMeta Meta { get; init; } = new();
[JsonPropertyName("links")]
public PageLinks? Links { get; init; }
[JsonPropertyName("actions")]
public IReadOnlyList<BinaryLaneAction> Actions { get; init; } = Array.Empty<BinaryLaneAction>();
}
public sealed class BalanceResponse : BinaryLaneDto
{
[JsonPropertyName("balance")]
public Balance Balance { get; init; } = new();
}
public sealed class InvoiceResponse : BinaryLaneDto
{
[JsonPropertyName("invoice")]
public Invoice Invoice { get; init; } = new();
}
public sealed class InvoicesResponse : BinaryLaneDto
{
[JsonPropertyName("meta")]
public PageMeta Meta { get; init; } = new();
[JsonPropertyName("links")]
public PageLinks? Links { get; init; }
[JsonPropertyName("invoices")]
public IReadOnlyList<Invoice> Invoices { get; init; } = Array.Empty<Invoice>();
}
public sealed class UnpaidFailedInvoicesResponse : BinaryLaneDto
{
[JsonPropertyName("invoices")]
public IReadOnlyList<Invoice> Invoices { get; init; } = Array.Empty<Invoice>();
}
public sealed class DataUsageResponse : BinaryLaneDto
{
[JsonPropertyName("data_usage")]
public DataUsage DataUsage { get; init; } = new();
}
public sealed class DataUsagesResponse : BinaryLaneDto
{
[JsonPropertyName("meta")]
public PageMeta Meta { get; init; } = new();
[JsonPropertyName("links")]
public PageLinks? Links { get; init; }
[JsonPropertyName("data_usages")]
public IReadOnlyList<DataUsage> DataUsages { get; init; } = Array.Empty<DataUsage>();
}
public sealed class LocalNameserversResponse : BinaryLaneDto
{
[JsonPropertyName("local_nameservers")]
public IReadOnlyList<string> LocalNameservers { get; init; } = Array.Empty<string>();
}
public sealed class DomainResponse : BinaryLaneDto
{
[JsonPropertyName("domain")]
public Domain Domain { get; init; } = new();
}
public sealed class DomainsResponse : BinaryLaneDto
{
[JsonPropertyName("meta")]
public PageMeta Meta { get; init; } = new();
[JsonPropertyName("links")]
public PageLinks? Links { get; init; }
[JsonPropertyName("domains")]
public IReadOnlyList<Domain> Domains { get; init; } = Array.Empty<Domain>();
}
public sealed class DomainRecordResponse : BinaryLaneDto
{
[JsonPropertyName("domain_record")]
public DomainRecord DomainRecord { get; init; } = new();
}
public sealed class DomainRecordsResponse : BinaryLaneDto
{
[JsonPropertyName("meta")]
public PageMeta Meta { get; init; } = new();
[JsonPropertyName("links")]
public PageLinks? Links { get; init; }
[JsonPropertyName("domain_records")]
public IReadOnlyList<DomainRecord> DomainRecords { get; init; } = Array.Empty<DomainRecord>();
}
public sealed class ImageResponse : BinaryLaneDto
{
[JsonPropertyName("image")]
public Image Image { get; init; } = new();
}
public sealed class ImagesResponse : BinaryLaneDto
{
[JsonPropertyName("meta")]
public PageMeta Meta { get; init; } = new();
[JsonPropertyName("links")]
public PageLinks? Links { get; init; }
[JsonPropertyName("images")]
public IReadOnlyList<Image> Images { get; init; } = Array.Empty<Image>();
}
public sealed class ImageDownloadResponse : BinaryLaneDto
{
[JsonPropertyName("link")]
public ImageDownload Link { get; init; } = new();
}
public sealed class SshKeyResponse : BinaryLaneDto
{
[JsonPropertyName("ssh_key")]
public SshKey SshKey { get; init; } = new();
}
public sealed class SshKeysResponse : BinaryLaneDto
{
[JsonPropertyName("meta")]
public PageMeta Meta { get; init; } = new();
[JsonPropertyName("links")]
public PageLinks? Links { get; init; }
[JsonPropertyName("ssh_keys")]
public IReadOnlyList<SshKey> SshKeys { get; init; } = Array.Empty<SshKey>();
}
public sealed class LoadBalancerResponse : BinaryLaneDto
{
[JsonPropertyName("load_balancer")]
public LoadBalancer LoadBalancer { get; init; } = new();
}
public sealed class LoadBalancersResponse : BinaryLaneDto
{
[JsonPropertyName("meta")]
public PageMeta Meta { get; init; } = new();
[JsonPropertyName("links")]
public PageLinks? Links { get; init; }
[JsonPropertyName("load_balancers")]
public IReadOnlyList<LoadBalancer> LoadBalancers { get; init; } = Array.Empty<LoadBalancer>();
}
public sealed class CreateLoadBalancerResponse : BinaryLaneDto
{
[JsonPropertyName("load_balancer")]
public LoadBalancer LoadBalancer { get; init; } = new();
[JsonPropertyName("links")]
public ActionsLinks Links { get; init; } = new();
}
public sealed class UpdateLoadBalancerResponse : BinaryLaneDto
{
[JsonPropertyName("load_balancer")]
public LoadBalancer LoadBalancer { get; init; } = new();
[JsonPropertyName("links")]
public ActionsLinks? Links { get; init; }
}
public sealed class LoadBalancerAvailabilityResponse : BinaryLaneDto
{
[JsonPropertyName("load_balancer_availability_options")]
public IReadOnlyList<LoadBalancerAvailabilityOption> LoadBalancerAvailabilityOptions { get; init; } = Array.Empty<LoadBalancerAvailabilityOption>();
}
public sealed class RegionsResponse : BinaryLaneDto
{
[JsonPropertyName("meta")]
public PageMeta Meta { get; init; } = new();
[JsonPropertyName("links")]
public PageLinks? Links { get; init; }
[JsonPropertyName("regions")]
public IReadOnlyList<Region> Regions { get; init; } = Array.Empty<Region>();
}
public sealed class ReverseNameServersResponse : BinaryLaneDto
{
[JsonPropertyName("meta")]
public PageMeta Meta { get; init; } = new();
[JsonPropertyName("links")]
public PageLinks? Links { get; init; }
[JsonPropertyName("reverse_nameservers")]
public IReadOnlyList<string> ReverseNameservers { get; init; } = Array.Empty<string>();
}
public sealed class SampleSetResponse : BinaryLaneDto
{
[JsonPropertyName("sample_set")]
public SampleSet? SampleSet { get; init; }
}
public sealed class SampleSetsResponse : BinaryLaneDto
{
[JsonPropertyName("meta")]
public PageMeta Meta { get; init; } = new();
[JsonPropertyName("links")]
public PageLinks? Links { get; init; }
[JsonPropertyName("sample_sets")]
public IReadOnlyList<SampleSet> SampleSets { get; init; } = Array.Empty<SampleSet>();
}
public sealed class ServerResponse : BinaryLaneDto
{
[JsonPropertyName("server")]
public Server Server { get; init; } = new();
}
public sealed class ServersResponse : BinaryLaneDto
{
[JsonPropertyName("meta")]
public PageMeta Meta { get; init; } = new();
[JsonPropertyName("links")]
public PageLinks? Links { get; init; }
[JsonPropertyName("servers")]
public IReadOnlyList<Server> Servers { get; init; } = Array.Empty<Server>();
}
public sealed class CreateServerResponse : BinaryLaneDto
{
[JsonPropertyName("server")]
public Server Server { get; init; } = new();
[JsonPropertyName("links")]
public ActionsLinks Links { get; init; } = new();
}
public sealed class AdvancedFirewallRulesResponse : BinaryLaneDto
{
[JsonPropertyName("firewall_rules")]
public IReadOnlyList<AdvancedFirewallRule> FirewallRules { get; init; } = Array.Empty<AdvancedFirewallRule>();
}
public sealed class AvailableAdvancedServerFeaturesResponse : BinaryLaneDto
{
[JsonPropertyName("available_advanced_server_features")]
public AvailableAdvancedServerFeatures AvailableAdvancedServerFeatures { get; init; } = new();
}
public sealed class BackupsResponse : BinaryLaneDto
{
[JsonPropertyName("meta")]
public PageMeta Meta { get; init; } = new();
[JsonPropertyName("links")]
public PageLinks? Links { get; init; }
[JsonPropertyName("backups")]
public IReadOnlyList<Image> Backups { get; init; } = Array.Empty<Image>();
}
public sealed class KernelsResponse : BinaryLaneDto
{
[JsonPropertyName("meta")]
public PageMeta Meta { get; init; } = new();
[JsonPropertyName("links")]
public PageLinks? Links { get; init; }
[JsonPropertyName("kernels")]
public IReadOnlyList<Kernel> Kernels { get; init; } = Array.Empty<Kernel>();
}
public sealed class SnapshotsResponse : BinaryLaneDto
{
[JsonPropertyName("meta")]
public PageMeta Meta { get; init; } = new();
[JsonPropertyName("links")]
public PageLinks? Links { get; init; }
[JsonPropertyName("snapshots")]
public IReadOnlyList<Image> Snapshots { get; init; } = Array.Empty<Image>();
}
public sealed class ThresholdAlertsResponse : BinaryLaneDto
{
[JsonPropertyName("threshold_alerts")]
public IReadOnlyList<ThresholdAlert> ThresholdAlerts { get; init; } = Array.Empty<ThresholdAlert>();
}
public sealed class CurrentServerAlertsResponse : BinaryLaneDto
{
[JsonPropertyName("server_ids")]
public IReadOnlyList<long> ServerIds { get; init; } = Array.Empty<long>();
}
public sealed class LicensedSoftwaresResponse : BinaryLaneDto
{
[JsonPropertyName("meta")]
public PageMeta Meta { get; init; } = new();
[JsonPropertyName("links")]
public PageLinks? Links { get; init; }
[JsonPropertyName("licensed_software")]
public IReadOnlyList<LicensedSoftware> LicensedSoftware { get; init; } = Array.Empty<LicensedSoftware>();
}
public sealed class ServerConsoleResponse : BinaryLaneDto
{
[JsonPropertyName("console")]
public ServerConsole Console { get; init; } = new();
}
public sealed class SizesResponse : BinaryLaneDto
{
[JsonPropertyName("meta")]
public PageMeta Meta { get; init; } = new();
[JsonPropertyName("links")]
public PageLinks? Links { get; init; }
[JsonPropertyName("sizes")]
public IReadOnlyList<Size> Sizes { get; init; } = Array.Empty<Size>();
}
public sealed class SoftwareResponse : BinaryLaneDto
{
[JsonPropertyName("software")]
public Software Software { get; init; } = new();
}
public sealed class SoftwaresResponse : BinaryLaneDto
{
[JsonPropertyName("meta")]
public PageMeta Meta { get; init; } = new();
[JsonPropertyName("links")]
public PageLinks? Links { get; init; }
[JsonPropertyName("software")]
public IReadOnlyList<Software> Software { get; init; } = Array.Empty<Software>();
}
public sealed class VpcResponse : BinaryLaneDto
{
[JsonPropertyName("vpc")]
public Vpc Vpc { get; init; } = new();
}
public sealed class VpcsResponse : BinaryLaneDto
{
[JsonPropertyName("meta")]
public PageMeta Meta { get; init; } = new();
[JsonPropertyName("links")]
public PageLinks? Links { get; init; }
[JsonPropertyName("vpcs")]
public IReadOnlyList<Vpc> Vpcs { get; init; } = Array.Empty<Vpc>();
}
public sealed class VpcMembersResponse : BinaryLaneDto
{
[JsonPropertyName("meta")]
public PageMeta Meta { get; init; } = new();
[JsonPropertyName("links")]
public PageLinks? Links { get; init; }
[JsonPropertyName("members")]
public IReadOnlyList<VpcMember> Members { get; init; } = Array.Empty<VpcMember>();
}
@@ -0,0 +1,209 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace BinaryLane.Api.V2.Models;
/// <summary>
/// Serializes the BinaryLane server-action discriminated union when callers hold
/// an action through its <see cref="ServerAction"/> base type.
/// </summary>
/// <remarks>
/// System.Text.Json normally uses the statically declared type. That would emit
/// only <c>type</c> for a <see cref="ServerAction"/> variable whose runtime
/// value is, for example, <see cref="ResizeServerAction"/>. The BinaryLane API
/// requires the concrete action properties as well, so this converter writes
/// the runtime payload and handles all documented discriminator values. Unknown
/// action values are preserved through <see cref="UnknownServerAction"/>.
/// </remarks>
public sealed class ServerActionJsonConverter : JsonConverter<ServerAction>
{
private static readonly PropertyInfo AdditionalPropertiesProperty =
typeof(BinaryLaneDto).GetProperty(nameof(BinaryLaneDto.AdditionalProperties))
?? throw new InvalidOperationException("BinaryLaneDto must expose extension data.");
/// <inheritdoc />
public override bool CanConvert(Type typeToConvert) => typeof(ServerAction).IsAssignableFrom(typeToConvert);
/// <inheritdoc />
public override ServerAction? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
using JsonDocument document = JsonDocument.ParseValue(ref reader);
if (document.RootElement.ValueKind == JsonValueKind.Null)
{
return null;
}
if (document.RootElement.ValueKind != JsonValueKind.Object)
{
throw new JsonException("A BinaryLane server action must be a JSON object.");
}
string type = ReadType(document.RootElement);
ServerAction action = CreateAction(type);
PopulateAction(action, document.RootElement, options);
return action;
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, ServerAction value, JsonSerializerOptions options)
{
writer.WriteStartObject();
writer.WriteString("type", value.Type);
var writtenProperties = new HashSet<string>(StringComparer.Ordinal)
{
"type",
};
foreach (PropertyInfo property in GetPayloadProperties(value.GetType()))
{
string jsonName = GetJsonName(property);
if (!writtenProperties.Add(jsonName))
{
continue;
}
object? propertyValue = property.GetValue(value);
if (propertyValue is null && options.DefaultIgnoreCondition == JsonIgnoreCondition.WhenWritingNull)
{
continue;
}
writer.WritePropertyName(jsonName);
JsonSerializer.Serialize(writer, propertyValue, property.PropertyType, options);
}
if (value.AdditionalProperties is not null)
{
foreach (KeyValuePair<string, JsonElement> property in value.AdditionalProperties)
{
if (!writtenProperties.Add(property.Key))
{
continue;
}
writer.WritePropertyName(property.Key);
property.Value.WriteTo(writer);
}
}
writer.WriteEndObject();
}
private static string ReadType(JsonElement action)
{
if (!action.TryGetProperty("type", out JsonElement typeElement) ||
typeElement.ValueKind != JsonValueKind.String ||
string.IsNullOrWhiteSpace(typeElement.GetString()))
{
throw new JsonException("A BinaryLane server action requires a non-empty 'type' discriminator.");
}
return typeElement.GetString()!;
}
private static ServerAction CreateAction(string type) => type switch
{
BinaryLaneValues.ServerActionType.AddDisk => new AddDiskServerAction(),
BinaryLaneValues.ServerActionType.AttachBackup => new AttachBackupServerAction(),
BinaryLaneValues.ServerActionType.ChangeAdvancedFeatures => new ChangeAdvancedFeaturesServerAction(),
BinaryLaneValues.ServerActionType.ChangeAdvancedFirewallRules => new ChangeAdvancedFirewallRulesServerAction(),
BinaryLaneValues.ServerActionType.ChangeBackupSchedule => new ChangeBackupScheduleServerAction(),
BinaryLaneValues.ServerActionType.ChangeIpv6 => new ChangeIpv6ServerAction(),
BinaryLaneValues.ServerActionType.ChangeIpv6ReverseNameservers => new ChangeIpv6ReverseNameserversServerAction(),
BinaryLaneValues.ServerActionType.ChangeKernel => new ChangeKernelServerAction(),
BinaryLaneValues.ServerActionType.ChangeManageOffsiteBackupCopies => new ChangeManageOffsiteBackupCopiesServerAction(),
BinaryLaneValues.ServerActionType.ChangeNetwork => new ChangeNetworkServerAction(),
BinaryLaneValues.ServerActionType.ChangeOffsiteBackupLocation => new ChangeOffsiteBackupLocationServerAction(),
BinaryLaneValues.ServerActionType.ChangePartner => new ChangePartnerServerAction(),
BinaryLaneValues.ServerActionType.ChangePortBlocking => new ChangePortBlockingServerAction(),
BinaryLaneValues.ServerActionType.ChangeRegion => new ChangeRegionServerAction(),
BinaryLaneValues.ServerActionType.ChangeReverseName => new ChangeReverseNameServerAction(),
BinaryLaneValues.ServerActionType.ChangeSeparatePrivateNetworkInterface => new ChangeSeparatePrivateNetworkInterfaceServerAction(),
BinaryLaneValues.ServerActionType.ChangeSourceAndDestinationCheck => new ChangeSourceAndDestinationCheckServerAction(),
BinaryLaneValues.ServerActionType.ChangeThresholdAlerts => new ChangeThresholdAlertsServerAction(),
BinaryLaneValues.ServerActionType.ChangeVpcIpv4 => new ChangeVpcIpv4ServerAction(),
BinaryLaneValues.ServerActionType.CloneUsingBackup => new CloneUsingBackupServerAction(),
BinaryLaneValues.ServerActionType.DeleteDisk => new DeleteDiskServerAction(),
BinaryLaneValues.ServerActionType.DetachBackup => new DetachBackupServerAction(),
BinaryLaneValues.ServerActionType.DisableBackups => new DisableBackupsServerAction(),
BinaryLaneValues.ServerActionType.DisableSelinux => new DisableSelinuxServerAction(),
BinaryLaneValues.ServerActionType.EnableBackups => new EnableBackupsServerAction(),
BinaryLaneValues.ServerActionType.EnableIpv6 => new EnableIpv6ServerAction(),
BinaryLaneValues.ServerActionType.IsRunning => new IsRunningServerAction(),
BinaryLaneValues.ServerActionType.PasswordReset => new PasswordResetServerAction(),
BinaryLaneValues.ServerActionType.Ping => new PingServerAction(),
BinaryLaneValues.ServerActionType.PowerCycle => new PowerCycleServerAction(),
BinaryLaneValues.ServerActionType.PowerOff => new PowerOffServerAction(),
BinaryLaneValues.ServerActionType.PowerOn => new PowerOnServerAction(),
BinaryLaneValues.ServerActionType.Reboot => new RebootServerAction(),
BinaryLaneValues.ServerActionType.Rebuild => new RebuildServerAction(),
BinaryLaneValues.ServerActionType.Rename => new RenameServerAction(),
BinaryLaneValues.ServerActionType.Resize => new ResizeServerAction(),
BinaryLaneValues.ServerActionType.ResizeDisk => new ResizeDiskServerAction(),
BinaryLaneValues.ServerActionType.Restore => new RestoreServerAction(),
BinaryLaneValues.ServerActionType.Shutdown => new ShutdownServerAction(),
BinaryLaneValues.ServerActionType.TakeBackup => new TakeBackupServerAction(),
BinaryLaneValues.ServerActionType.Uncancel => new UncancelServerAction(),
BinaryLaneValues.ServerActionType.Uptime => new UptimeServerAction(),
_ => new UnknownServerAction(type),
};
private static void PopulateAction(ServerAction action, JsonElement source, JsonSerializerOptions options)
{
var knownProperties = new HashSet<string>(StringComparer.Ordinal)
{
"type",
};
foreach (PropertyInfo property in GetPayloadProperties(action.GetType()))
{
string jsonName = GetJsonName(property);
knownProperties.Add(jsonName);
if (!source.TryGetProperty(jsonName, out JsonElement value))
{
continue;
}
object? deserialized = JsonSerializer.Deserialize(value.GetRawText(), property.PropertyType, options);
property.SetValue(action, deserialized);
}
var additionalProperties = new Dictionary<string, JsonElement>(StringComparer.Ordinal);
foreach (JsonProperty property in source.EnumerateObject())
{
if (!knownProperties.Contains(property.Name))
{
additionalProperties[property.Name] = property.Value.Clone();
}
}
if (additionalProperties.Count > 0)
{
AdditionalPropertiesProperty.SetValue(action, additionalProperties);
}
}
private static IEnumerable<PropertyInfo> GetPayloadProperties(Type actionType)
{
foreach (PropertyInfo property in actionType.GetProperties(BindingFlags.Instance | BindingFlags.Public))
{
if (!property.CanRead || !property.CanWrite ||
property.Name == nameof(ServerAction.Type) ||
property.Name == nameof(BinaryLaneDto.AdditionalProperties) ||
property.GetIndexParameters().Length != 0 ||
property.GetCustomAttribute<JsonIgnoreAttribute>()?.Condition == JsonIgnoreCondition.Always)
{
continue;
}
yield return property;
}
}
private static string GetJsonName(PropertyInfo property) =>
property.GetCustomAttribute<JsonPropertyNameAttribute>()?.Name ?? property.Name;
}
@@ -0,0 +1,521 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace BinaryLane.Api.V2.Models;
/// <summary>
/// Base payload for <c>POST /v2/servers/{server_id}/actions</c>.
/// </summary>
/// <remarks>
/// The upstream contract is a discriminated union on <c>type</c>. Its public
/// documentation also creates fragment-bearing pseudo-paths for each variant;
/// those are not separate HTTP routes. Prefer a concrete derived type for a
/// documented action. <see cref="UnknownServerAction"/> permits new provider
/// action types without waiting for an SDK release.
/// </remarks>
public class ServerAction : BinaryLaneRequestModel
{
/// <summary>Creates an empty action for serializers and advanced callers.</summary>
public ServerAction()
{
}
/// <summary>Creates a typed action with the provider discriminator value.</summary>
protected ServerAction(string type)
{
Type = type;
}
[JsonPropertyName("type")]
public string Type { get; init; } = string.Empty;
}
/// <summary>An action type not yet represented by a concrete SDK class.</summary>
public sealed class UnknownServerAction : ServerAction
{
public UnknownServerAction(string type)
: base(type)
{
}
}
/// <summary>Adds an additional disk to a server.</summary>
public sealed class AddDiskServerAction : ServerAction
{
public AddDiskServerAction() : base("add_disk") { }
[JsonPropertyName("size_gigabytes")]
public int SizeGigabytes { get; init; }
[JsonPropertyName("description")]
public string? Description { get; init; }
}
/// <summary>Attaches a backup image to a server.</summary>
public sealed class AttachBackupServerAction : ServerAction
{
public AttachBackupServerAction() : base("attach_backup") { }
[JsonPropertyName("image")]
public long ImageId { get; init; }
}
/// <summary>Changes advanced virtual-machine features.</summary>
public sealed class ChangeAdvancedFeaturesServerAction : ServerAction
{
public ChangeAdvancedFeaturesServerAction() : base("change_advanced_features") { }
[JsonPropertyName("enabled_advanced_features")]
public IReadOnlyList<string>? EnabledAdvancedFeatures { get; init; }
[JsonPropertyName("processor_model")]
public long? ProcessorModel { get; init; }
[JsonPropertyName("automatic_processor_model")]
public bool? AutomaticProcessorModel { get; init; }
[JsonPropertyName("machine_type")]
public string? MachineType { get; init; }
[JsonPropertyName("automatic_machine_type")]
public bool? AutomaticMachineType { get; init; }
[JsonPropertyName("video_device")]
public string? VideoDevice { get; init; }
}
/// <summary>Replaces the advanced firewall rules on a server.</summary>
public sealed class ChangeAdvancedFirewallRulesServerAction : ServerAction
{
public ChangeAdvancedFirewallRulesServerAction() : base("change_advanced_firewall_rules") { }
[JsonPropertyName("firewall_rules")]
public IReadOnlyList<AdvancedFirewallRuleRequest> FirewallRules { get; init; } = Array.Empty<AdvancedFirewallRuleRequest>();
}
/// <summary>Changes a server backup schedule.</summary>
public sealed class ChangeBackupScheduleServerAction : ServerAction
{
public ChangeBackupScheduleServerAction() : base("change_backup_schedule") { }
[JsonPropertyName("backup_hour_of_day")]
public int? BackupHourOfDay { get; init; }
[JsonPropertyName("backup_day_of_week")]
public int? BackupDayOfWeek { get; init; }
[JsonPropertyName("backup_day_of_month")]
public int? BackupDayOfMonth { get; init; }
}
/// <summary>Enables or disables IPv6 for a server.</summary>
public sealed class ChangeIpv6ServerAction : ServerAction
{
public ChangeIpv6ServerAction() : base("change_ipv6") { }
[JsonPropertyName("enabled")]
public bool Enabled { get; init; }
}
/// <summary>Changes a server's IPv6 reverse nameservers.</summary>
public sealed class ChangeIpv6ReverseNameserversServerAction : ServerAction
{
public ChangeIpv6ReverseNameserversServerAction() : base("change_ipv6_reverse_nameservers") { }
[JsonPropertyName("ipv6_reverse_nameservers")]
public IReadOnlyList<string> Ipv6ReverseNameservers { get; init; } = Array.Empty<string>();
}
/// <summary>Changes a server kernel.</summary>
public sealed class ChangeKernelServerAction : ServerAction
{
public ChangeKernelServerAction() : base("change_kernel") { }
[JsonPropertyName("kernel")]
public long KernelId { get; init; }
}
/// <summary>Changes whether offsite backup copies are managed automatically.</summary>
public sealed class ChangeManageOffsiteBackupCopiesServerAction : ServerAction
{
public ChangeManageOffsiteBackupCopiesServerAction() : base("change_manage_offsite_backup_copies") { }
[JsonPropertyName("manage_offsite_backup_copies")]
public bool ManageOffsiteBackupCopies { get; init; }
}
/// <summary>Moves a server onto or off a VPC network.</summary>
public sealed class ChangeNetworkServerAction : ServerAction
{
public ChangeNetworkServerAction() : base("change_network") { }
[JsonPropertyName("vpc_id")]
public long? VpcId { get; init; }
}
/// <summary>Changes the server's offsite backup location.</summary>
public sealed class ChangeOffsiteBackupLocationServerAction : ServerAction
{
public ChangeOffsiteBackupLocationServerAction() : base("change_offsite_backup_location") { }
[JsonPropertyName("offsite_backup_location")]
public string? OffsiteBackupLocation { get; init; }
}
/// <summary>Changes the server's partner server relationship.</summary>
public sealed class ChangePartnerServerAction : ServerAction
{
public ChangePartnerServerAction() : base("change_partner") { }
[JsonPropertyName("partner_server_id")]
public long? PartnerServerId { get; init; }
}
/// <summary>Enables or disables port blocking.</summary>
public sealed class ChangePortBlockingServerAction : ServerAction
{
public ChangePortBlockingServerAction() : base("change_port_blocking") { }
[JsonPropertyName("enabled")]
public bool Enabled { get; init; }
}
/// <summary>Moves a server to a new region.</summary>
public sealed class ChangeRegionServerAction : ServerAction
{
public ChangeRegionServerAction() : base("change_region") { }
[JsonPropertyName("region")]
public string Region { get; init; } = string.Empty;
}
/// <summary>Changes an IPv4 reverse name.</summary>
public sealed class ChangeReverseNameServerAction : ServerAction
{
public ChangeReverseNameServerAction() : base("change_reverse_name") { }
[JsonPropertyName("ipv4_address")]
public string Ipv4Address { get; init; } = string.Empty;
[JsonPropertyName("reverse_name")]
public string? ReverseName { get; init; }
}
/// <summary>Enables or disables a separate private network interface.</summary>
public sealed class ChangeSeparatePrivateNetworkInterfaceServerAction : ServerAction
{
public ChangeSeparatePrivateNetworkInterfaceServerAction() : base("change_separate_private_network_interface") { }
[JsonPropertyName("enabled")]
public bool Enabled { get; init; }
}
/// <summary>Enables or disables source-and-destination checking.</summary>
public sealed class ChangeSourceAndDestinationCheckServerAction : ServerAction
{
public ChangeSourceAndDestinationCheckServerAction() : base("change_source_and_destination_check") { }
[JsonPropertyName("enabled")]
public bool Enabled { get; init; }
}
/// <summary>Replaces a server's threshold-alert configuration.</summary>
public sealed class ChangeThresholdAlertsServerAction : ServerAction
{
public ChangeThresholdAlertsServerAction() : base("change_threshold_alerts") { }
[JsonPropertyName("threshold_alerts")]
public IReadOnlyList<ThresholdAlertRequest> ThresholdAlerts { get; init; } = Array.Empty<ThresholdAlertRequest>();
}
/// <summary>Changes a VPC IPv4 address on a server.</summary>
public sealed class ChangeVpcIpv4ServerAction : ServerAction
{
public ChangeVpcIpv4ServerAction() : base("change_vpc_ipv4") { }
[JsonPropertyName("current_ipv4_address")]
public string CurrentIpv4Address { get; init; } = string.Empty;
[JsonPropertyName("new_ipv4_address")]
public string NewIpv4Address { get; init; } = string.Empty;
}
/// <summary>Clones a backup image onto another server.</summary>
public sealed class CloneUsingBackupServerAction : ServerAction
{
public CloneUsingBackupServerAction() : base("clone_using_backup") { }
[JsonPropertyName("image_id")]
public long ImageId { get; init; }
[JsonPropertyName("target_server_id")]
public long TargetServerId { get; init; }
[JsonPropertyName("name")]
public string? Name { get; init; }
}
/// <summary>Deletes a non-primary disk.</summary>
public sealed class DeleteDiskServerAction : ServerAction
{
public DeleteDiskServerAction() : base("delete_disk") { }
[JsonPropertyName("disk_id")]
public long DiskId { get; init; }
}
/// <summary>Detaches an attached backup from a server.</summary>
public sealed class DetachBackupServerAction : ServerAction
{
public DetachBackupServerAction() : base("detach_backup") { }
}
/// <summary>Disables automatic backups.</summary>
public sealed class DisableBackupsServerAction : ServerAction
{
public DisableBackupsServerAction() : base("disable_backups") { }
}
/// <summary>Disables SELinux in a compatible installed distribution.</summary>
public sealed class DisableSelinuxServerAction : ServerAction
{
public DisableSelinuxServerAction() : base("disable_selinux") { }
}
/// <summary>Enables automatic backups.</summary>
public sealed class EnableBackupsServerAction : ServerAction
{
public EnableBackupsServerAction() : base("enable_backups") { }
}
/// <summary>Enables IPv6 on a server.</summary>
public sealed class EnableIpv6ServerAction : ServerAction
{
public EnableIpv6ServerAction() : base("enable_ipv6") { }
}
/// <summary>Checks whether a server is running.</summary>
public sealed class IsRunningServerAction : ServerAction
{
public IsRunningServerAction() : base("is_running") { }
}
/// <summary>Resets a server password.</summary>
public sealed class PasswordResetServerAction : ServerAction
{
public PasswordResetServerAction() : base("password_reset") { }
[JsonPropertyName("username")]
public string? Username { get; init; }
/// <remarks>Do not log this value.</remarks>
[JsonPropertyName("password")]
public string? Password { get; init; }
}
/// <summary>Pings a server.</summary>
public sealed class PingServerAction : ServerAction
{
public PingServerAction() : base("ping") { }
}
/// <summary>Power cycles a server.</summary>
public sealed class PowerCycleServerAction : ServerAction
{
public PowerCycleServerAction() : base("power_cycle") { }
}
/// <summary>Powers a server off.</summary>
public sealed class PowerOffServerAction : ServerAction
{
public PowerOffServerAction() : base("power_off") { }
}
/// <summary>Powers a server on.</summary>
public sealed class PowerOnServerAction : ServerAction
{
public PowerOnServerAction() : base("power_on") { }
}
/// <summary>Reboots a server.</summary>
public sealed class RebootServerAction : ServerAction
{
public RebootServerAction() : base("reboot") { }
}
/// <summary>Rebuilds a server from an image.</summary>
public sealed class RebuildServerAction : ServerAction
{
public RebuildServerAction() : base("rebuild") { }
/// <summary>An image ID or image slug. Omit to reuse the existing image.</summary>
[JsonPropertyName("image")]
public object? Image { get; init; }
[JsonPropertyName("options")]
public ImageOptions? Options { get; init; }
}
/// <summary>Renames a server.</summary>
public sealed class RenameServerAction : ServerAction
{
public RenameServerAction() : base("rename") { }
[JsonPropertyName("name")]
public string Name { get; init; } = string.Empty;
}
/// <summary>Changes selected size options during a resize action.</summary>
public sealed class ChangeSizeOptionsRequest : BinaryLaneRequestModel
{
[JsonPropertyName("daily_backups")]
public int? DailyBackups { get; init; }
[JsonPropertyName("weekly_backups")]
public int? WeeklyBackups { get; init; }
[JsonPropertyName("monthly_backups")]
public int? MonthlyBackups { get; init; }
[JsonPropertyName("offsite_backups")]
public bool? OffsiteBackups { get; init; }
[JsonPropertyName("ipv4_addresses")]
public int? Ipv4Addresses { get; init; }
[JsonPropertyName("memory")]
public int? MemoryMegabytes { get; init; }
[JsonPropertyName("disk")]
public int? DiskGigabytes { get; init; }
[JsonPropertyName("transfer")]
public double? TransferGigabytes { get; init; }
[JsonPropertyName("ipv4_addresses_to_remove")]
public IReadOnlyList<string>? Ipv4AddressesToRemove { get; init; }
}
/// <summary>Changes the source image selected by a resize operation.</summary>
public sealed class ChangeImage : BinaryLaneRequestModel
{
/// <summary>An image ID or image slug.</summary>
[JsonPropertyName("image")]
public object? Image { get; init; }
[JsonPropertyName("options")]
public ImageOptions? Options { get; init; }
}
/// <summary>Changes licences selected by a resize operation.</summary>
public sealed class ChangeLicenses : BinaryLaneRequestModel
{
[JsonPropertyName("licenses")]
public IReadOnlyList<License> Licenses { get; init; } = Array.Empty<License>();
}
/// <summary>Resizes a server.</summary>
public sealed class ResizeServerAction : ServerAction
{
public ResizeServerAction() : base("resize") { }
[JsonPropertyName("size")]
public string? Size { get; init; }
[JsonPropertyName("options")]
public ChangeSizeOptionsRequest? Options { get; init; }
[JsonPropertyName("change_image")]
public ChangeImage? ChangeImage { get; init; }
[JsonPropertyName("change_licenses")]
public ChangeLicenses? ChangeLicenses { get; init; }
[JsonPropertyName("pre_action_backup")]
public TakeBackupServerAction? PreActionBackup { get; init; }
}
/// <summary>Resizes an individual server disk.</summary>
public sealed class ResizeDiskServerAction : ServerAction
{
public ResizeDiskServerAction() : base("resize_disk") { }
[JsonPropertyName("disk_id")]
public long DiskId { get; init; }
[JsonPropertyName("size_gigabytes")]
public int SizeGigabytes { get; init; }
}
/// <summary>Restores a server from an image.</summary>
public sealed class RestoreServerAction : ServerAction
{
public RestoreServerAction() : base("restore") { }
/// <summary>An image ID or image slug.</summary>
[JsonPropertyName("image")]
public object Image { get; init; } = string.Empty;
}
/// <summary>Requests a graceful shutdown.</summary>
public sealed class ShutdownServerAction : ServerAction
{
public ShutdownServerAction() : base("shutdown") { }
}
/// <summary>Creates a backup of a server.</summary>
public sealed class TakeBackupServerAction : ServerAction
{
public TakeBackupServerAction() : base("take_backup") { }
/// <summary>Provider backup slot, such as <c>daily</c> or <c>temporary</c>.</summary>
[JsonPropertyName("backup_type")]
public string? BackupType { get; init; }
/// <summary>Provider replacement strategy.</summary>
[JsonPropertyName("replacement_strategy")]
public string ReplacementStrategy { get; init; } = string.Empty;
[JsonPropertyName("backup_id_to_replace")]
public long? BackupIdToReplace { get; init; }
[JsonPropertyName("label")]
public string? Label { get; init; }
}
/// <summary>Reverses a scheduled server cancellation.</summary>
public sealed class UncancelServerAction : ServerAction
{
public UncancelServerAction() : base("uncancel") { }
}
/// <summary>Checks server uptime.</summary>
public sealed class UptimeServerAction : ServerAction
{
public UptimeServerAction() : base("uptime") { }
}
/// <summary>An advanced firewall rule supplied in a server-action request.</summary>
public sealed class AdvancedFirewallRuleRequest : BinaryLaneRequestModel
{
[JsonPropertyName("source_addresses")]
public IReadOnlyList<string> SourceAddresses { get; init; } = Array.Empty<string>();
[JsonPropertyName("destination_addresses")]
public IReadOnlyList<string> DestinationAddresses { get; init; } = Array.Empty<string>();
[JsonPropertyName("destination_ports")]
public IReadOnlyList<string>? DestinationPorts { get; init; }
/// <summary>Provider protocol value, such as <c>tcp</c>.</summary>
[JsonPropertyName("protocol")]
public string Protocol { get; init; } = string.Empty;
/// <summary>Provider action value, such as <c>accept</c>.</summary>
[JsonPropertyName("action")]
public string Action { get; init; } = string.Empty;
[JsonPropertyName("description")]
public string? Description { get; init; }
}
+44
View File
@@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace BinaryLane.Api.V2.Pagination;
/// <summary>A page returned from a BinaryLane list endpoint.</summary>
/// <typeparam name="T">Item type.</typeparam>
public sealed class Page<T>
{
internal Page(
IReadOnlyList<T> items,
int total,
Uri? firstPage,
Uri? previousPage,
Uri? nextPage,
Uri? lastPage)
{
Items = new ReadOnlyCollection<T>(new List<T>(items));
Total = total;
FirstPage = firstPage;
PreviousPage = previousPage;
NextPage = nextPage;
LastPage = lastPage;
}
/// <summary>Items returned in this response.</summary>
public IReadOnlyList<T> Items { get; }
/// <summary>Total number of items available across all pages, when supplied by BinaryLane.</summary>
public int Total { get; }
/// <summary>First result page, when supplied by BinaryLane.</summary>
public Uri? FirstPage { get; }
/// <summary>Previous result page, when supplied by BinaryLane.</summary>
public Uri? PreviousPage { get; }
/// <summary>Next result page, when supplied by BinaryLane.</summary>
public Uri? NextPage { get; }
/// <summary>Last result page, when supplied by BinaryLane.</summary>
public Uri? LastPage { get; }
}
@@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
namespace BinaryLane.Api.V2.Pagination;
/// <summary>Specifies a one-based BinaryLane API result page.</summary>
public sealed class PageRequest
{
/// <summary>One-based page number.</summary>
public int Page { get; set; } = 1;
/// <summary>Number of results to return, from 1 through 200.</summary>
public int PerPage { get; set; } = 20;
internal IReadOnlyDictionary<string, object?> ToQuery()
{
if (Page < 1)
{
throw new ArgumentOutOfRangeException(nameof(Page), "Page numbering starts at 1.");
}
if (PerPage < 1 || PerPage > 200)
{
throw new ArgumentOutOfRangeException(nameof(PerPage), "PerPage must be between 1 and 200.");
}
return new Dictionary<string, object?>
{
["page"] = Page,
["per_page"] = PerPage,
};
}
}
@@ -0,0 +1,28 @@
using System.Net;
using System.Text.Json;
using BinaryLane.Api.V2.Models;
namespace BinaryLane.Api.V2.Resources;
/// <summary>Result of submitting an asynchronous server action.</summary>
public sealed class ActionSubmission
{
internal ActionSubmission(HttpStatusCode statusCode, BinaryLaneAction? action, JsonElement response)
{
StatusCode = statusCode;
Action = action;
Response = response;
}
/// <summary>HTTP status returned by BinaryLane, commonly 200 or 202.</summary>
public HttpStatusCode StatusCode { get; }
/// <summary>The action supplied by BinaryLane, when present in the response.</summary>
public BinaryLaneAction? Action { get; }
/// <summary>Raw response for forward-compatible access to provider fields.</summary>
public JsonElement Response { get; }
/// <summary>Whether BinaryLane accepted the request for asynchronous completion.</summary>
public bool IsAccepted => StatusCode == HttpStatusCode.Accepted;
}
@@ -0,0 +1,26 @@
using System;
namespace BinaryLane.Api.V2.Resources;
/// <summary>Controls opt-in polling of a BinaryLane asynchronous action.</summary>
public sealed class ActionWaitOptions
{
/// <summary>Delay between action status checks.</summary>
public TimeSpan PollInterval { get; set; } = TimeSpan.FromSeconds(2);
/// <summary>Maximum time spent polling before a timeout is reported.</summary>
public TimeSpan Timeout { get; set; } = TimeSpan.FromMinutes(15);
internal void Validate()
{
if (PollInterval < TimeSpan.FromMilliseconds(100) || PollInterval > TimeSpan.FromMinutes(5))
{
throw new ArgumentOutOfRangeException(nameof(PollInterval), "PollInterval must be between 100 milliseconds and five minutes.");
}
if (Timeout < PollInterval || Timeout > TimeSpan.FromHours(24))
{
throw new ArgumentOutOfRangeException(nameof(Timeout), "Timeout must be at least PollInterval and no more than 24 hours.");
}
}
}
@@ -0,0 +1,293 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using BinaryLane.Api.V2.Http;
using BinaryLane.Api.V2.Pagination;
namespace BinaryLane.Api.V2.Resources;
/// <summary>Shared JSON-envelope and pagination behavior for resource APIs.</summary>
public abstract class BinaryLaneResourceBase
{
/// <summary>Initializes a resource using the common request executor.</summary>
protected BinaryLaneResourceBase(IBinaryLaneApiExecutor executor, BinaryLaneJsonSerializerOptions json)
{
Executor = executor ?? throw new ArgumentNullException(nameof(executor));
Json = json ?? throw new ArgumentNullException(nameof(json));
}
/// <summary>Common request executor.</summary>
protected IBinaryLaneApiExecutor Executor { get; }
/// <summary>Common JSON options.</summary>
protected BinaryLaneJsonSerializerOptions Json { get; }
/// <summary>Gets a raw JSON response, allowing forward-compatible access to a newly added API field.</summary>
protected Task<JsonElement> GetRawAsync(
string path,
IReadOnlyDictionary<string, object?>? query = null,
CancellationToken cancellationToken = default) =>
Executor.GetAsync<JsonElement>(path, query, cancellationToken);
/// <summary>Sends a raw JSON mutation request.</summary>
protected async Task<JsonElement> SendRawAsync(
System.Net.Http.HttpMethod method,
string path,
object? body = null,
IReadOnlyDictionary<string, object?>? query = null,
CancellationToken cancellationToken = default)
{
var response = await Executor.SendAsync<JsonElement>(
new BinaryLaneRequest(method, path) { Body = body, Query = query }, cancellationToken).ConfigureAwait(false);
return response.Body;
}
/// <summary>Sends a raw request and retains the HTTP response metadata.</summary>
protected Task<BinaryLaneResponse<JsonElement>> SendResponseAsync(
System.Net.Http.HttpMethod method,
string path,
object? body = null,
IReadOnlyDictionary<string, object?>? query = null,
CancellationToken cancellationToken = default) =>
Executor.SendAsync<JsonElement>(
new BinaryLaneRequest(method, path) { Body = body, Query = query }, cancellationToken);
/// <summary>Sends a request expected to have no response body.</summary>
protected async Task SendNoContentAsync(
System.Net.Http.HttpMethod method,
string path,
object? body = null,
IReadOnlyDictionary<string, object?>? query = null,
CancellationToken cancellationToken = default)
{
await Executor.SendAsync(
new BinaryLaneRequest(method, path) { Body = body, Query = query }, cancellationToken).ConfigureAwait(false);
}
/// <summary>Gets a response whose value is wrapped in a named JSON property.</summary>
protected async Task<T> GetItemAsync<T>(
string path,
string propertyName,
CancellationToken cancellationToken = default)
{
var document = await GetRawAsync(path, null, cancellationToken).ConfigureAwait(false);
return DeserializeEnvelope<T>(document, propertyName);
}
/// <summary>Gets a response whose JSON body is the requested value rather than an envelope.</summary>
protected async Task<T> GetDirectItemAsync<T>(
string path,
CancellationToken cancellationToken = default)
{
var document = await GetRawAsync(path, null, cancellationToken).ConfigureAwait(false);
var item = JsonSerializer.Deserialize<T>(document.GetRawText(), Json.SerializerOptions);
if (item is null)
{
throw new JsonException("BinaryLane response did not contain a usable JSON value.");
}
return item;
}
/// <summary>Sends a mutation whose value is wrapped in a named JSON property.</summary>
protected async Task<T> SendItemAsync<T>(
System.Net.Http.HttpMethod method,
string path,
object? body,
string propertyName,
CancellationToken cancellationToken = default)
{
var document = await Executor.SendAsync<JsonElement>(
new BinaryLaneRequest(method, path) { Body = body }, cancellationToken).ConfigureAwait(false);
return DeserializeEnvelope<T>(document.Body, propertyName);
}
/// <summary>Gets a typed page whose items are wrapped in a named JSON property.</summary>
protected async Task<Page<T>> GetPageAsync<T>(
string path,
string propertyName,
PageRequest? page = null,
IReadOnlyDictionary<string, object?>? query = null,
CancellationToken cancellationToken = default)
{
var parameters = MergeQuery(query, page?.ToQuery());
var document = await GetRawAsync(path, parameters, cancellationToken).ConfigureAwait(false);
return DeserializePage<T>(document, propertyName);
}
/// <summary>Enumerates every available page without enabling retries for mutating requests.</summary>
protected async IAsyncEnumerable<T> GetAllPagesAsync<T>(
string path,
string propertyName,
PageRequest? page = null,
IReadOnlyDictionary<string, object?>? query = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
var seenNextPageLinks = new HashSet<string>(StringComparer.Ordinal);
var current = await GetPageAsync<T>(path, propertyName, page, query, cancellationToken).ConfigureAwait(false);
foreach (var item in current.Items)
{
yield return item;
}
while (current.NextPage is not null)
{
cancellationToken.ThrowIfCancellationRequested();
var nextPagePath = current.NextPage.IsAbsoluteUri
? current.NextPage.AbsoluteUri
: current.NextPage.OriginalString;
if (!seenNextPageLinks.Add(nextPagePath))
{
throw new InvalidOperationException(
$"BinaryLane returned a repeated pagination link: '{nextPagePath}'.");
}
current = await GetPageAsync<T>(
nextPagePath,
propertyName,
null,
null,
cancellationToken).ConfigureAwait(false);
foreach (var item in current.Items)
{
yield return item;
}
}
}
/// <summary>Deserializes an item from a standard BinaryLane JSON response envelope.</summary>
protected T DeserializeEnvelope<T>(JsonElement document, string propertyName)
{
if (document.ValueKind != JsonValueKind.Object ||
!document.TryGetProperty(propertyName, out var payload))
{
throw new JsonException($"BinaryLane response did not contain the expected '{propertyName}' envelope property.");
}
var item = JsonSerializer.Deserialize<T>(payload.GetRawText(), Json.SerializerOptions);
if (item is null)
{
throw new JsonException($"BinaryLane response did not contain a usable '{propertyName}' value.");
}
return item;
}
/// <summary>Attempts to deserialize a value only when the named envelope property is present.</summary>
protected bool TryDeserializeEnvelope<T>(JsonElement document, string propertyName, out T? item)
{
item = default;
if (document.ValueKind != JsonValueKind.Object || !document.TryGetProperty(propertyName, out var payload))
{
return false;
}
item = JsonSerializer.Deserialize<T>(payload.GetRawText(), Json.SerializerOptions);
return item is not null;
}
/// <summary>Deserializes a standard BinaryLane list response.</summary>
protected Page<T> DeserializePage<T>(JsonElement document, string propertyName)
{
if (document.ValueKind != JsonValueKind.Object ||
!document.TryGetProperty(propertyName, out var collection) ||
collection.ValueKind != JsonValueKind.Array)
{
throw new JsonException($"BinaryLane response did not contain the expected '{propertyName}' array.");
}
var items = new List<T>();
foreach (var value in collection.EnumerateArray())
{
var item = JsonSerializer.Deserialize<T>(value.GetRawText(), Json.SerializerOptions);
if (item is null)
{
throw new JsonException(
$"BinaryLane response contained an unusable item in the '{propertyName}' array.");
}
items.Add(item);
}
var total = items.Count;
if (document.TryGetProperty("meta", out var meta) &&
meta.ValueKind == JsonValueKind.Object &&
meta.TryGetProperty("total", out var totalValue) &&
totalValue.TryGetInt32(out var providedTotal))
{
total = providedTotal;
}
Uri? first = null;
Uri? previous = null;
Uri? next = null;
Uri? last = null;
if (document.TryGetProperty("links", out var links) &&
links.ValueKind == JsonValueKind.Object &&
links.TryGetProperty("pages", out var pages) &&
pages.ValueKind == JsonValueKind.Object)
{
first = ReadUri(pages, "first");
previous = ReadUri(pages, "prev");
next = ReadUri(pages, "next");
last = ReadUri(pages, "last");
}
return new Page<T>(items, total, first, previous, next, last);
}
private static Uri? ReadUri(JsonElement element, string name)
{
if (!element.TryGetProperty(name, out var value) || value.ValueKind != JsonValueKind.String)
{
return null;
}
return Uri.TryCreate(value.GetString(), UriKind.RelativeOrAbsolute, out var uri) ? uri : null;
}
/// <summary>Escapes an arbitrary API path segment.</summary>
protected static string EscapePathSegment(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
throw new ArgumentException("A path segment is required.", nameof(value));
}
return Uri.EscapeDataString(value);
}
private static Dictionary<string, object?>? MergeQuery(
IReadOnlyDictionary<string, object?>? primary,
IReadOnlyDictionary<string, object?>? secondary)
{
if (primary is null && secondary is null)
{
return null;
}
var result = new Dictionary<string, object?>();
if (primary is not null)
{
foreach (var pair in primary)
{
result[pair.Key] = pair.Value;
}
}
if (secondary is not null)
{
foreach (var pair in secondary)
{
result[pair.Key] = pair.Value;
}
}
return result;
}
}
@@ -0,0 +1,337 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using BinaryLane.Api.V2.Http;
using BinaryLane.Api.V2.Models;
using BinaryLane.Api.V2.Pagination;
namespace BinaryLane.Api.V2.Resources;
/// <inheritdoc />
public sealed class AccountApi : BinaryLaneResourceBase, IAccountApi
{
/// <summary>Initializes the account resource.</summary>
public AccountApi(IBinaryLaneApiExecutor executor, BinaryLaneJsonSerializerOptions json)
: base(executor, json)
{
}
/// <inheritdoc />
public Task<Account> GetAsync(CancellationToken cancellationToken = default) =>
GetItemAsync<Account>("v2/account", "account", cancellationToken);
/// <inheritdoc />
public Task<JsonElement> GetRawAsync(CancellationToken cancellationToken = default) =>
base.GetRawAsync("v2/account", null, cancellationToken);
}
/// <inheritdoc />
public sealed class ActionsApi : BinaryLaneResourceBase, IActionsApi
{
/// <summary>Initializes the actions resource.</summary>
public ActionsApi(IBinaryLaneApiExecutor executor, BinaryLaneJsonSerializerOptions json)
: base(executor, json)
{
}
/// <inheritdoc />
public Task<BinaryLaneAction> GetAsync(long actionId, CancellationToken cancellationToken = default) =>
GetItemAsync<BinaryLaneAction>($"v2/actions/{actionId}", "action", cancellationToken);
/// <inheritdoc />
public Task<Page<BinaryLaneAction>> ListAsync(PageRequest? page = null, CancellationToken cancellationToken = default) =>
GetPageAsync<BinaryLaneAction>("v2/actions", "actions", page, null, cancellationToken);
/// <inheritdoc />
public IAsyncEnumerable<BinaryLaneAction> ListAllAsync(PageRequest? page = null, CancellationToken cancellationToken = default) =>
GetAllPagesAsync<BinaryLaneAction>("v2/actions", "actions", page, null, cancellationToken);
/// <inheritdoc />
public Task ProceedAsync(long actionId, ProceedRequest request, CancellationToken cancellationToken = default) =>
SendNoContentAsync(HttpMethod.Post, $"v2/actions/{actionId}/proceed", request ?? throw new ArgumentNullException(nameof(request)), null, cancellationToken);
/// <inheritdoc />
public async Task<BinaryLaneAction> WaitForCompletionAsync(
long actionId,
ActionWaitOptions? options = null,
CancellationToken cancellationToken = default)
{
options ??= new ActionWaitOptions();
options.Validate();
var startedAt = DateTimeOffset.UtcNow;
while (true)
{
cancellationToken.ThrowIfCancellationRequested();
var action = await GetAsync(actionId, cancellationToken).ConfigureAwait(false);
if (IsTerminal(action.Status))
{
return action;
}
if (DateTimeOffset.UtcNow - startedAt >= options.Timeout)
{
throw new TimeoutException(
$"Timed out waiting for BinaryLane action {actionId} after {options.Timeout}.");
}
await Task.Delay(options.PollInterval, cancellationToken).ConfigureAwait(false);
}
}
private static bool IsTerminal(string status) =>
string.Equals(status, "completed", StringComparison.OrdinalIgnoreCase) ||
string.Equals(status, "errored", StringComparison.OrdinalIgnoreCase) ||
string.Equals(status, "failed", StringComparison.OrdinalIgnoreCase) ||
string.Equals(status, "cancelled", StringComparison.OrdinalIgnoreCase) ||
string.Equals(status, "canceled", StringComparison.OrdinalIgnoreCase);
}
/// <inheritdoc />
public sealed class BillingApi : BinaryLaneResourceBase, IBillingApi
{
/// <summary>Initializes the billing resource.</summary>
public BillingApi(IBinaryLaneApiExecutor executor, BinaryLaneJsonSerializerOptions json)
: base(executor, json)
{
}
/// <inheritdoc />
public Task<Balance> GetBalanceAsync(CancellationToken cancellationToken = default) =>
GetItemAsync<Balance>("v2/customers/my/balance", "balance", cancellationToken);
/// <inheritdoc />
public Task<Invoice> GetInvoiceAsync(long invoiceId, CancellationToken cancellationToken = default) =>
GetItemAsync<Invoice>($"v2/customers/my/invoices/{invoiceId}", "invoice", cancellationToken);
/// <inheritdoc />
public Task<Page<Invoice>> ListInvoicesAsync(PageRequest? page = null, CancellationToken cancellationToken = default) =>
GetPageAsync<Invoice>("v2/customers/my/invoices", "invoices", page, null, cancellationToken);
/// <inheritdoc />
public Task<IReadOnlyList<Invoice>> ListUnpaidPaymentFailedInvoicesAsync(CancellationToken cancellationToken = default) =>
GetItemAsync<IReadOnlyList<Invoice>>("v2/customers/my/unpaid-payment-failed-invoices", "invoices", cancellationToken);
}
/// <inheritdoc />
public sealed class DataUsageApi : BinaryLaneResourceBase, IDataUsageApi
{
/// <summary>Initializes the data-usage resource.</summary>
public DataUsageApi(IBinaryLaneApiExecutor executor, BinaryLaneJsonSerializerOptions json)
: base(executor, json)
{
}
/// <inheritdoc />
public Task<Page<DataUsage>> ListCurrentAsync(PageRequest? page = null, CancellationToken cancellationToken = default) =>
GetPageAsync<DataUsage>("v2/data_usages/current", "data_usages", page, null, cancellationToken);
/// <inheritdoc />
public IAsyncEnumerable<DataUsage> ListAllCurrentAsync(PageRequest? page = null, CancellationToken cancellationToken = default) =>
GetAllPagesAsync<DataUsage>("v2/data_usages/current", "data_usages", page, null, cancellationToken);
/// <inheritdoc />
public Task<DataUsage> GetCurrentAsync(long serverId, CancellationToken cancellationToken = default) =>
GetItemAsync<DataUsage>($"v2/data_usages/{serverId}/current", "data_usage", cancellationToken);
}
/// <inheritdoc />
public sealed class DomainsApi : BinaryLaneResourceBase, IDomainsApi
{
/// <summary>Initializes the domains resource.</summary>
public DomainsApi(IBinaryLaneApiExecutor executor, BinaryLaneJsonSerializerOptions json)
: base(executor, json)
{
}
/// <inheritdoc />
public Task<IReadOnlyList<string>> GetNameserversAsync(CancellationToken cancellationToken = default) =>
GetItemAsync<IReadOnlyList<string>>("v2/domains/nameservers", "local_nameservers", cancellationToken);
/// <inheritdoc />
public Task RefreshNameserverCacheAsync(DomainRefreshRequest request, CancellationToken cancellationToken = default) =>
SendNoContentAsync(HttpMethod.Post, "v2/domains/refresh_nameserver_cache", request ?? throw new ArgumentNullException(nameof(request)), null, cancellationToken);
/// <inheritdoc />
public Task<Page<Domain>> ListAsync(PageRequest? page = null, CancellationToken cancellationToken = default) =>
GetPageAsync<Domain>("v2/domains", "domains", page, null, cancellationToken);
/// <inheritdoc />
public IAsyncEnumerable<Domain> ListAllAsync(PageRequest? page = null, CancellationToken cancellationToken = default) =>
GetAllPagesAsync<Domain>("v2/domains", "domains", page, null, cancellationToken);
/// <inheritdoc />
public Task<Domain> CreateAsync(DomainRequest request, CancellationToken cancellationToken = default) =>
SendItemAsync<Domain>(HttpMethod.Post, "v2/domains", request ?? throw new ArgumentNullException(nameof(request)), "domain", cancellationToken);
/// <inheritdoc />
public Task<Domain> GetAsync(string domainName, CancellationToken cancellationToken = default) =>
GetItemAsync<Domain>($"v2/domains/{EscapePathSegment(domainName)}", "domain", cancellationToken);
/// <inheritdoc />
public Task DeleteAsync(string domainName, CancellationToken cancellationToken = default) =>
Executor.DeleteAsync($"v2/domains/{EscapePathSegment(domainName)}", null, null, cancellationToken);
/// <inheritdoc />
public Task<Page<DomainRecord>> ListRecordsAsync(string domainName, PageRequest? page = null, CancellationToken cancellationToken = default) =>
GetPageAsync<DomainRecord>($"v2/domains/{EscapePathSegment(domainName)}/records", "domain_records", page, null, cancellationToken);
/// <inheritdoc />
public Task<DomainRecord> CreateRecordAsync(string domainName, DomainRecordRequest request, CancellationToken cancellationToken = default) =>
SendItemAsync<DomainRecord>(HttpMethod.Post, $"v2/domains/{EscapePathSegment(domainName)}/records", request ?? throw new ArgumentNullException(nameof(request)), "domain_record", cancellationToken);
/// <inheritdoc />
public Task<DomainRecord> GetRecordAsync(string domainName, long recordId, CancellationToken cancellationToken = default) =>
GetItemAsync<DomainRecord>($"v2/domains/{EscapePathSegment(domainName)}/records/{recordId}", "domain_record", cancellationToken);
/// <inheritdoc />
public Task<DomainRecord> UpdateRecordAsync(string domainName, long recordId, UpdateDomainRecordRequest request, CancellationToken cancellationToken = default) =>
SendItemAsync<DomainRecord>(HttpMethod.Put, $"v2/domains/{EscapePathSegment(domainName)}/records/{recordId}", request ?? throw new ArgumentNullException(nameof(request)), "domain_record", cancellationToken);
/// <inheritdoc />
public Task DeleteRecordAsync(string domainName, long recordId, CancellationToken cancellationToken = default) =>
Executor.DeleteAsync($"v2/domains/{EscapePathSegment(domainName)}/records/{recordId}", null, null, cancellationToken);
}
/// <inheritdoc />
public sealed class ImagesApi : BinaryLaneResourceBase, IImagesApi
{
/// <summary>Initializes the images resource.</summary>
public ImagesApi(IBinaryLaneApiExecutor executor, BinaryLaneJsonSerializerOptions json)
: base(executor, json)
{
}
/// <inheritdoc />
public Task<Page<Image>> ListAsync(PageRequest? page = null, CancellationToken cancellationToken = default) =>
GetPageAsync<Image>("v2/images", "images", page, null, cancellationToken);
/// <inheritdoc />
public IAsyncEnumerable<Image> ListAllAsync(PageRequest? page = null, CancellationToken cancellationToken = default) =>
GetAllPagesAsync<Image>("v2/images", "images", page, null, cancellationToken);
/// <inheritdoc />
public Task<Image> GetAsync(string imageIdOrSlug, CancellationToken cancellationToken = default) =>
GetItemAsync<Image>($"v2/images/{EscapePathSegment(imageIdOrSlug)}", "image", cancellationToken);
/// <inheritdoc />
public Task<Image> UpdateAsync(long imageId, ImageRequest request, CancellationToken cancellationToken = default) =>
SendItemAsync<Image>(HttpMethod.Put, $"v2/images/{imageId}", request ?? throw new ArgumentNullException(nameof(request)), "image", cancellationToken);
/// <inheritdoc />
public Task<ImageDownload> GetDownloadAsync(long imageId, CancellationToken cancellationToken = default) =>
GetItemAsync<ImageDownload>($"v2/images/{imageId}/download", "link", cancellationToken);
}
/// <inheritdoc />
public sealed class SshKeysApi : BinaryLaneResourceBase, ISshKeysApi
{
/// <summary>Initializes the SSH-key resource.</summary>
public SshKeysApi(IBinaryLaneApiExecutor executor, BinaryLaneJsonSerializerOptions json)
: base(executor, json)
{
}
/// <inheritdoc />
public Task<Page<SshKey>> ListAsync(PageRequest? page = null, CancellationToken cancellationToken = default) =>
GetPageAsync<SshKey>("v2/account/keys", "ssh_keys", page, null, cancellationToken);
/// <inheritdoc />
public IAsyncEnumerable<SshKey> ListAllAsync(PageRequest? page = null, CancellationToken cancellationToken = default) =>
GetAllPagesAsync<SshKey>("v2/account/keys", "ssh_keys", page, null, cancellationToken);
/// <inheritdoc />
public Task<SshKey> GetAsync(long keyId, CancellationToken cancellationToken = default) =>
GetItemAsync<SshKey>($"v2/account/keys/{keyId}", "ssh_key", cancellationToken);
/// <inheritdoc />
public Task<SshKey> CreateAsync(SshKeyRequest request, CancellationToken cancellationToken = default) =>
SendItemAsync<SshKey>(HttpMethod.Post, "v2/account/keys", request ?? throw new ArgumentNullException(nameof(request)), "ssh_key", cancellationToken);
/// <inheritdoc />
public Task<SshKey> UpdateAsync(long keyId, UpdateSshKeyRequest request, CancellationToken cancellationToken = default) =>
SendItemAsync<SshKey>(HttpMethod.Put, $"v2/account/keys/{keyId}", request ?? throw new ArgumentNullException(nameof(request)), "ssh_key", cancellationToken);
/// <inheritdoc />
public Task DeleteAsync(long keyId, CancellationToken cancellationToken = default) =>
Executor.DeleteAsync($"v2/account/keys/{keyId}", null, null, cancellationToken);
}
/// <inheritdoc />
public sealed class LoadBalancersApi : BinaryLaneResourceBase, ILoadBalancersApi
{
/// <summary>Initializes the load-balancer resource.</summary>
public LoadBalancersApi(IBinaryLaneApiExecutor executor, BinaryLaneJsonSerializerOptions json)
: base(executor, json)
{
}
/// <inheritdoc />
public Task<Page<LoadBalancer>> ListAsync(PageRequest? page = null, CancellationToken cancellationToken = default) =>
GetPageAsync<LoadBalancer>("v2/load_balancers", "load_balancers", page, null, cancellationToken);
/// <inheritdoc />
public IAsyncEnumerable<LoadBalancer> ListAllAsync(PageRequest? page = null, CancellationToken cancellationToken = default) =>
GetAllPagesAsync<LoadBalancer>("v2/load_balancers", "load_balancers", page, null, cancellationToken);
/// <inheritdoc />
public Task<LoadBalancer> GetAsync(long loadBalancerId, CancellationToken cancellationToken = default) =>
GetItemAsync<LoadBalancer>($"v2/load_balancers/{loadBalancerId}", "load_balancer", cancellationToken);
/// <inheritdoc />
public Task<LoadBalancer> CreateAsync(CreateLoadBalancerRequest request, CancellationToken cancellationToken = default) =>
SendItemAsync<LoadBalancer>(HttpMethod.Post, "v2/load_balancers", request ?? throw new ArgumentNullException(nameof(request)), "load_balancer", cancellationToken);
/// <inheritdoc />
public Task<LoadBalancer> UpdateAsync(long loadBalancerId, UpdateLoadBalancerRequest request, CancellationToken cancellationToken = default) =>
SendItemAsync<LoadBalancer>(HttpMethod.Put, $"v2/load_balancers/{loadBalancerId}", request ?? throw new ArgumentNullException(nameof(request)), "load_balancer", cancellationToken);
/// <inheritdoc />
public Task DeleteAsync(long loadBalancerId, CancellationToken cancellationToken = default) =>
Executor.DeleteAsync($"v2/load_balancers/{loadBalancerId}", null, null, cancellationToken);
/// <inheritdoc />
public Task<IReadOnlyList<LoadBalancerAvailabilityOption>> GetAvailabilityAsync(CancellationToken cancellationToken = default) =>
GetItemAsync<IReadOnlyList<LoadBalancerAvailabilityOption>>(
"v2/load_balancers/availability",
"load_balancer_availability_options",
cancellationToken);
/// <inheritdoc />
public Task AddServersAsync(long loadBalancerId, ServerIdsRequest request, CancellationToken cancellationToken = default) =>
SendNoContentAsync(HttpMethod.Post, $"v2/load_balancers/{loadBalancerId}/servers", request ?? throw new ArgumentNullException(nameof(request)), null, cancellationToken);
/// <inheritdoc />
public Task RemoveServersAsync(long loadBalancerId, ServerIdsRequest request, CancellationToken cancellationToken = default) =>
Executor.DeleteAsync($"v2/load_balancers/{loadBalancerId}/servers", request ?? throw new ArgumentNullException(nameof(request)), null, cancellationToken);
/// <inheritdoc />
public Task AddForwardingRulesAsync(long loadBalancerId, ForwardingRulesRequest request, CancellationToken cancellationToken = default) =>
SendNoContentAsync(HttpMethod.Post, $"v2/load_balancers/{loadBalancerId}/forwarding_rules", request ?? throw new ArgumentNullException(nameof(request)), null, cancellationToken);
/// <inheritdoc />
public Task RemoveForwardingRulesAsync(long loadBalancerId, ForwardingRulesRequest request, CancellationToken cancellationToken = default) =>
Executor.DeleteAsync($"v2/load_balancers/{loadBalancerId}/forwarding_rules", request ?? throw new ArgumentNullException(nameof(request)), null, cancellationToken);
}
/// <inheritdoc />
public sealed class RegionsApi : BinaryLaneResourceBase, IRegionsApi
{
/// <summary>Initializes the regions resource.</summary>
public RegionsApi(IBinaryLaneApiExecutor executor, BinaryLaneJsonSerializerOptions json)
: base(executor, json)
{
}
/// <inheritdoc />
public Task<Page<Region>> ListAsync(PageRequest? page = null, CancellationToken cancellationToken = default) =>
GetPageAsync<Region>("v2/regions", "regions", page, null, cancellationToken);
/// <inheritdoc />
public IAsyncEnumerable<Region> ListAllAsync(PageRequest? page = null, CancellationToken cancellationToken = default) =>
GetAllPagesAsync<Region>("v2/regions", "regions", page, null, cancellationToken);
}
@@ -0,0 +1,286 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using BinaryLane.Api.V2.Http;
using BinaryLane.Api.V2.Models;
using BinaryLane.Api.V2.Pagination;
namespace BinaryLane.Api.V2.Resources;
/// <inheritdoc />
public sealed class ReverseNamesApi : BinaryLaneResourceBase, IReverseNamesApi
{
/// <summary>Initializes the reverse-names resource.</summary>
public ReverseNamesApi(IBinaryLaneApiExecutor executor, BinaryLaneJsonSerializerOptions json)
: base(executor, json)
{
}
/// <inheritdoc />
public Task<Page<string>> ListIpv6Async(PageRequest? page = null, CancellationToken cancellationToken = default) =>
GetPageAsync<string>("v2/reverse_names/ipv6", "reverse_nameservers", page, null, cancellationToken);
/// <inheritdoc />
public IAsyncEnumerable<string> ListAllIpv6Async(PageRequest? page = null, CancellationToken cancellationToken = default) =>
GetAllPagesAsync<string>("v2/reverse_names/ipv6", "reverse_nameservers", page, null, cancellationToken);
/// <inheritdoc />
public async Task<ActionSubmission> UpdateIpv6Async(ReverseNameserversRequest request, CancellationToken cancellationToken = default)
{
var response = await SendResponseAsync(
HttpMethod.Post,
"v2/reverse_names/ipv6",
request ?? throw new ArgumentNullException(nameof(request)),
null,
cancellationToken).ConfigureAwait(false);
TryDeserializeEnvelope<BinaryLaneAction>(response.Body, "action", out var action);
return new ActionSubmission(response.StatusCode, action, response.Body);
}
}
/// <inheritdoc />
public sealed class SampleSetsApi : BinaryLaneResourceBase, ISampleSetsApi
{
/// <summary>Initializes the monitoring sample-set resource.</summary>
public SampleSetsApi(IBinaryLaneApiExecutor executor, BinaryLaneJsonSerializerOptions json)
: base(executor, json)
{
}
/// <inheritdoc />
public async Task<SampleSet?> GetLatestAsync(long serverId, string? dataInterval = null, CancellationToken cancellationToken = default)
{
IReadOnlyDictionary<string, object?>? query = string.IsNullOrWhiteSpace(dataInterval)
? null
: new Dictionary<string, object?> { ["data_interval"] = dataInterval };
var response = await GetRawAsync($"v2/samplesets/{serverId}/latest", query, cancellationToken).ConfigureAwait(false);
return TryDeserializeEnvelope<SampleSet>(response, "sample_set", out var sampleSet) ? sampleSet : null;
}
/// <inheritdoc />
public Task<Page<SampleSet>> ListAsync(
long serverId,
PageRequest? page = null,
string? dataInterval = null,
DateTimeOffset? start = null,
DateTimeOffset? endAt = null,
CancellationToken cancellationToken = default)
{
var query = new Dictionary<string, object?>
{
["data_interval"] = dataInterval,
["start"] = start,
["end"] = endAt,
};
return GetPageAsync<SampleSet>($"v2/samplesets/{serverId}", "sample_sets", page, query, cancellationToken);
}
}
/// <inheritdoc />
public sealed class ServersApi : BinaryLaneResourceBase, IServersApi
{
/// <summary>Initializes the servers resource.</summary>
public ServersApi(IBinaryLaneApiExecutor executor, BinaryLaneJsonSerializerOptions json)
: base(executor, json)
{
}
/// <inheritdoc />
public Task<Server> GetAsync(long serverId, CancellationToken cancellationToken = default) =>
GetItemAsync<Server>($"v2/servers/{serverId}", "server", cancellationToken);
/// <inheritdoc />
public Task<Page<Server>> ListAsync(PageRequest? page = null, CancellationToken cancellationToken = default) =>
GetPageAsync<Server>("v2/servers", "servers", page, null, cancellationToken);
/// <inheritdoc />
public IAsyncEnumerable<Server> ListAllAsync(PageRequest? page = null, CancellationToken cancellationToken = default) =>
GetAllPagesAsync<Server>("v2/servers", "servers", page, null, cancellationToken);
/// <inheritdoc />
public Task<Server> CreateAsync(CreateServerRequest request, CancellationToken cancellationToken = default) =>
SendItemAsync<Server>(HttpMethod.Post, "v2/servers", request ?? throw new ArgumentNullException(nameof(request)), "server", cancellationToken);
/// <inheritdoc />
public Task DeleteAsync(long serverId, string? reason = null, CancellationToken cancellationToken = default) =>
Executor.DeleteAsync(
$"v2/servers/{serverId}",
null,
string.IsNullOrWhiteSpace(reason) ? null : new Dictionary<string, object?> { ["reason"] = reason },
cancellationToken);
/// <inheritdoc />
public Task<Page<BinaryLaneAction>> ListActionsAsync(long serverId, PageRequest? page = null, CancellationToken cancellationToken = default) =>
GetPageAsync<BinaryLaneAction>($"v2/servers/{serverId}/actions", "actions", page, null, cancellationToken);
/// <inheritdoc />
public Task<BinaryLaneAction> GetActionAsync(long serverId, long actionId, CancellationToken cancellationToken = default) =>
GetItemAsync<BinaryLaneAction>($"v2/servers/{serverId}/actions/{actionId}", "action", cancellationToken);
/// <inheritdoc />
public async Task<ActionSubmission> SubmitActionAsync(long serverId, ServerAction request, CancellationToken cancellationToken = default)
{
#if NET8_0_OR_GREATER
ArgumentNullException.ThrowIfNull(request);
#else
if (request is null)
{
throw new ArgumentNullException(nameof(request));
}
#endif
var response = await SendResponseAsync(
HttpMethod.Post,
$"v2/servers/{serverId}/actions",
request,
null,
cancellationToken).ConfigureAwait(false);
TryDeserializeEnvelope<BinaryLaneAction>(response.Body, "action", out var action);
return new ActionSubmission(response.StatusCode, action, response.Body);
}
/// <inheritdoc />
public Task<IReadOnlyList<AdvancedFirewallRule>> GetAdvancedFirewallRulesAsync(long serverId, CancellationToken cancellationToken = default) =>
GetItemAsync<IReadOnlyList<AdvancedFirewallRule>>(
$"v2/servers/{serverId}/advanced_firewall_rules",
"firewall_rules",
cancellationToken);
/// <inheritdoc />
public Task<AvailableAdvancedServerFeatures> GetAvailableAdvancedFeaturesAsync(long serverId, CancellationToken cancellationToken = default) =>
GetItemAsync<AvailableAdvancedServerFeatures>(
$"v2/servers/{serverId}/available_advanced_features",
"available_advanced_server_features",
cancellationToken);
/// <inheritdoc />
public Task<Page<Image>> ListBackupsAsync(long serverId, PageRequest? page = null, CancellationToken cancellationToken = default) =>
GetPageAsync<Image>($"v2/servers/{serverId}/backups", "backups", page, null, cancellationToken);
/// <inheritdoc />
public Task<BinaryLaneAction> CreateBackupAsync(long serverId, UploadImageRequest request, CancellationToken cancellationToken = default) =>
SendItemAsync<BinaryLaneAction>(HttpMethod.Post, $"v2/servers/{serverId}/backups", request ?? throw new ArgumentNullException(nameof(request)), "action", cancellationToken);
/// <inheritdoc />
public Task<Page<Kernel>> ListKernelsAsync(long serverId, PageRequest? page = null, CancellationToken cancellationToken = default) =>
GetPageAsync<Kernel>($"v2/servers/{serverId}/kernels", "kernels", page, null, cancellationToken);
/// <inheritdoc />
public Task<Page<Image>> ListSnapshotsAsync(long serverId, PageRequest? page = null, CancellationToken cancellationToken = default) =>
GetPageAsync<Image>($"v2/servers/{serverId}/snapshots", "snapshots", page, null, cancellationToken);
/// <inheritdoc />
public Task<IReadOnlyList<ThresholdAlert>> GetThresholdAlertsAsync(long serverId, CancellationToken cancellationToken = default) =>
GetItemAsync<IReadOnlyList<ThresholdAlert>>($"v2/servers/{serverId}/threshold_alerts", "threshold_alerts", cancellationToken);
/// <inheritdoc />
public Task<IReadOnlyList<long>> GetThresholdAlertServerIdsAsync(CancellationToken cancellationToken = default) =>
GetItemAsync<IReadOnlyList<long>>("v2/servers/threshold_alerts", "server_ids", cancellationToken);
/// <inheritdoc />
public Task<Page<LicensedSoftware>> ListSoftwareAsync(long serverId, PageRequest? page = null, CancellationToken cancellationToken = default) =>
GetPageAsync<LicensedSoftware>($"v2/servers/{serverId}/software", "licensed_software", page, null, cancellationToken);
/// <inheritdoc />
public Task<UserData> GetUserDataAsync(long serverId, CancellationToken cancellationToken = default) =>
GetDirectItemAsync<UserData>($"v2/servers/{serverId}/user_data", cancellationToken);
/// <inheritdoc />
public Task<ServerConsole> GetConsoleAsync(long serverId, CancellationToken cancellationToken = default) =>
GetItemAsync<ServerConsole>($"v2/servers/{serverId}/console", "console", cancellationToken);
}
/// <inheritdoc />
public sealed class SizesApi : BinaryLaneResourceBase, ISizesApi
{
/// <summary>Initializes the sizes resource.</summary>
public SizesApi(IBinaryLaneApiExecutor executor, BinaryLaneJsonSerializerOptions json)
: base(executor, json)
{
}
/// <inheritdoc />
public Task<Page<Size>> ListAsync(PageRequest? page = null, CancellationToken cancellationToken = default) =>
GetPageAsync<Size>("v2/sizes", "sizes", page, null, cancellationToken);
/// <inheritdoc />
public IAsyncEnumerable<Size> ListAllAsync(PageRequest? page = null, CancellationToken cancellationToken = default) =>
GetAllPagesAsync<Size>("v2/sizes", "sizes", page, null, cancellationToken);
}
/// <inheritdoc />
public sealed class SoftwareApi : BinaryLaneResourceBase, ISoftwareApi
{
/// <summary>Initializes the software resource.</summary>
public SoftwareApi(IBinaryLaneApiExecutor executor, BinaryLaneJsonSerializerOptions json)
: base(executor, json)
{
}
/// <inheritdoc />
public Task<Page<Software>> ListAsync(PageRequest? page = null, CancellationToken cancellationToken = default) =>
GetPageAsync<Software>("v2/software", "software", page, null, cancellationToken);
/// <inheritdoc />
public IAsyncEnumerable<Software> ListAllAsync(PageRequest? page = null, CancellationToken cancellationToken = default) =>
GetAllPagesAsync<Software>("v2/software", "software", page, null, cancellationToken);
/// <inheritdoc />
public Task<Software> GetAsync(long softwareId, CancellationToken cancellationToken = default) =>
GetItemAsync<Software>($"v2/software/{softwareId}", "software", cancellationToken);
/// <inheritdoc />
public Task<Page<Software>> ListOperatingSystemAsync(string operatingSystemIdOrSlug, PageRequest? page = null, CancellationToken cancellationToken = default) =>
GetPageAsync<Software>(
$"v2/software/operating_system/{EscapePathSegment(operatingSystemIdOrSlug)}",
"software",
page,
null,
cancellationToken);
}
/// <inheritdoc />
public sealed class VpcsApi : BinaryLaneResourceBase, IVpcsApi
{
/// <summary>Initializes the VPC resource.</summary>
public VpcsApi(IBinaryLaneApiExecutor executor, BinaryLaneJsonSerializerOptions json)
: base(executor, json)
{
}
/// <inheritdoc />
public Task<Page<Vpc>> ListAsync(PageRequest? page = null, CancellationToken cancellationToken = default) =>
GetPageAsync<Vpc>("v2/vpcs", "vpcs", page, null, cancellationToken);
/// <inheritdoc />
public IAsyncEnumerable<Vpc> ListAllAsync(PageRequest? page = null, CancellationToken cancellationToken = default) =>
GetAllPagesAsync<Vpc>("v2/vpcs", "vpcs", page, null, cancellationToken);
/// <inheritdoc />
public Task<Vpc> GetAsync(long vpcId, CancellationToken cancellationToken = default) =>
GetItemAsync<Vpc>($"v2/vpcs/{vpcId}", "vpc", cancellationToken);
/// <inheritdoc />
public Task<Vpc> CreateAsync(CreateVpcRequest request, CancellationToken cancellationToken = default) =>
SendItemAsync<Vpc>(HttpMethod.Post, "v2/vpcs", request ?? throw new ArgumentNullException(nameof(request)), "vpc", cancellationToken);
/// <inheritdoc />
public Task<Vpc> ReplaceAsync(long vpcId, UpdateVpcRequest request, CancellationToken cancellationToken = default) =>
SendItemAsync<Vpc>(HttpMethod.Put, $"v2/vpcs/{vpcId}", request ?? throw new ArgumentNullException(nameof(request)), "vpc", cancellationToken);
/// <inheritdoc />
public Task<Vpc> UpdateAsync(long vpcId, PatchVpcRequest request, CancellationToken cancellationToken = default) =>
SendItemAsync<Vpc>(new HttpMethod("PATCH"), $"v2/vpcs/{vpcId}", request ?? throw new ArgumentNullException(nameof(request)), "vpc", cancellationToken);
/// <inheritdoc />
public Task DeleteAsync(long vpcId, CancellationToken cancellationToken = default) =>
Executor.DeleteAsync($"v2/vpcs/{vpcId}", null, null, cancellationToken);
/// <inheritdoc />
public Task<Page<VpcMember>> ListMembersAsync(long vpcId, PageRequest? page = null, CancellationToken cancellationToken = default) =>
GetPageAsync<VpcMember>($"v2/vpcs/{vpcId}/members", "members", page, null, cancellationToken);
}
@@ -0,0 +1,186 @@
using System.Collections.Generic;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using BinaryLane.Api.V2.Models;
using BinaryLane.Api.V2.Pagination;
namespace BinaryLane.Api.V2.Resources;
/// <summary>Reads information about the authenticated account.</summary>
public interface IAccountApi
{
/// <summary>Gets the current account.</summary>
Task<Account> GetAsync(CancellationToken cancellationToken = default);
/// <summary>Gets the untyped account response for forward compatibility.</summary>
Task<JsonElement> GetRawAsync(CancellationToken cancellationToken = default);
}
/// <summary>Reads and waits for asynchronous account and server actions.</summary>
public interface IActionsApi
{
/// <summary>Gets an action by its global action ID.</summary>
Task<BinaryLaneAction> GetAsync(long actionId, CancellationToken cancellationToken = default);
/// <summary>Lists global actions.</summary>
Task<Page<BinaryLaneAction>> ListAsync(PageRequest? page = null, CancellationToken cancellationToken = default);
/// <summary>Streams all global actions.</summary>
IAsyncEnumerable<BinaryLaneAction> ListAllAsync(PageRequest? page = null, CancellationToken cancellationToken = default);
/// <summary>Supplies a requested user interaction for an action.</summary>
Task ProceedAsync(long actionId, ProceedRequest request, CancellationToken cancellationToken = default);
/// <summary>Polls an action until BinaryLane reports a terminal state.</summary>
Task<BinaryLaneAction> WaitForCompletionAsync(
long actionId,
ActionWaitOptions? options = null,
CancellationToken cancellationToken = default);
}
/// <summary>Reads account billing and invoice information.</summary>
public interface IBillingApi
{
Task<Balance> GetBalanceAsync(CancellationToken cancellationToken = default);
Task<Invoice> GetInvoiceAsync(long invoiceId, CancellationToken cancellationToken = default);
Task<Page<Invoice>> ListInvoicesAsync(PageRequest? page = null, CancellationToken cancellationToken = default);
Task<IReadOnlyList<Invoice>> ListUnpaidPaymentFailedInvoicesAsync(CancellationToken cancellationToken = default);
}
/// <summary>Reads data-transfer usage data.</summary>
public interface IDataUsageApi
{
Task<Page<DataUsage>> ListCurrentAsync(PageRequest? page = null, CancellationToken cancellationToken = default);
IAsyncEnumerable<DataUsage> ListAllCurrentAsync(PageRequest? page = null, CancellationToken cancellationToken = default);
Task<DataUsage> GetCurrentAsync(long serverId, CancellationToken cancellationToken = default);
}
/// <summary>Manages DNS domains and DNS records.</summary>
public interface IDomainsApi
{
Task<IReadOnlyList<string>> GetNameserversAsync(CancellationToken cancellationToken = default);
Task RefreshNameserverCacheAsync(DomainRefreshRequest request, CancellationToken cancellationToken = default);
Task<Page<Domain>> ListAsync(PageRequest? page = null, CancellationToken cancellationToken = default);
IAsyncEnumerable<Domain> ListAllAsync(PageRequest? page = null, CancellationToken cancellationToken = default);
Task<Domain> CreateAsync(DomainRequest request, CancellationToken cancellationToken = default);
Task<Domain> GetAsync(string domainName, CancellationToken cancellationToken = default);
Task DeleteAsync(string domainName, CancellationToken cancellationToken = default);
Task<Page<DomainRecord>> ListRecordsAsync(string domainName, PageRequest? page = null, CancellationToken cancellationToken = default);
Task<DomainRecord> CreateRecordAsync(string domainName, DomainRecordRequest request, CancellationToken cancellationToken = default);
Task<DomainRecord> GetRecordAsync(string domainName, long recordId, CancellationToken cancellationToken = default);
Task<DomainRecord> UpdateRecordAsync(string domainName, long recordId, UpdateDomainRecordRequest request, CancellationToken cancellationToken = default);
Task DeleteRecordAsync(string domainName, long recordId, CancellationToken cancellationToken = default);
}
/// <summary>Reads and manages account images and backups represented as images.</summary>
public interface IImagesApi
{
Task<Page<Image>> ListAsync(PageRequest? page = null, CancellationToken cancellationToken = default);
IAsyncEnumerable<Image> ListAllAsync(PageRequest? page = null, CancellationToken cancellationToken = default);
Task<Image> GetAsync(string imageIdOrSlug, CancellationToken cancellationToken = default);
Task<Image> UpdateAsync(long imageId, ImageRequest request, CancellationToken cancellationToken = default);
Task<ImageDownload> GetDownloadAsync(long imageId, CancellationToken cancellationToken = default);
}
/// <summary>Manages account SSH keys.</summary>
public interface ISshKeysApi
{
Task<Page<SshKey>> ListAsync(PageRequest? page = null, CancellationToken cancellationToken = default);
IAsyncEnumerable<SshKey> ListAllAsync(PageRequest? page = null, CancellationToken cancellationToken = default);
Task<SshKey> GetAsync(long keyId, CancellationToken cancellationToken = default);
Task<SshKey> CreateAsync(SshKeyRequest request, CancellationToken cancellationToken = default);
Task<SshKey> UpdateAsync(long keyId, UpdateSshKeyRequest request, CancellationToken cancellationToken = default);
Task DeleteAsync(long keyId, CancellationToken cancellationToken = default);
}
/// <summary>Manages load balancers, their members, and forwarding rules.</summary>
public interface ILoadBalancersApi
{
Task<Page<LoadBalancer>> ListAsync(PageRequest? page = null, CancellationToken cancellationToken = default);
IAsyncEnumerable<LoadBalancer> ListAllAsync(PageRequest? page = null, CancellationToken cancellationToken = default);
Task<LoadBalancer> GetAsync(long loadBalancerId, CancellationToken cancellationToken = default);
Task<LoadBalancer> CreateAsync(CreateLoadBalancerRequest request, CancellationToken cancellationToken = default);
Task<LoadBalancer> UpdateAsync(long loadBalancerId, UpdateLoadBalancerRequest request, CancellationToken cancellationToken = default);
Task DeleteAsync(long loadBalancerId, CancellationToken cancellationToken = default);
Task<IReadOnlyList<LoadBalancerAvailabilityOption>> GetAvailabilityAsync(CancellationToken cancellationToken = default);
Task AddServersAsync(long loadBalancerId, ServerIdsRequest request, CancellationToken cancellationToken = default);
Task RemoveServersAsync(long loadBalancerId, ServerIdsRequest request, CancellationToken cancellationToken = default);
Task AddForwardingRulesAsync(long loadBalancerId, ForwardingRulesRequest request, CancellationToken cancellationToken = default);
Task RemoveForwardingRulesAsync(long loadBalancerId, ForwardingRulesRequest request, CancellationToken cancellationToken = default);
}
/// <summary>Lists BinaryLane regions.</summary>
public interface IRegionsApi
{
Task<Page<Region>> ListAsync(PageRequest? page = null, CancellationToken cancellationToken = default);
IAsyncEnumerable<Region> ListAllAsync(PageRequest? page = null, CancellationToken cancellationToken = default);
}
/// <summary>Manages IPv6 reverse-name configuration.</summary>
public interface IReverseNamesApi
{
Task<Page<string>> ListIpv6Async(PageRequest? page = null, CancellationToken cancellationToken = default);
IAsyncEnumerable<string> ListAllIpv6Async(PageRequest? page = null, CancellationToken cancellationToken = default);
Task<ActionSubmission> UpdateIpv6Async(ReverseNameserversRequest request, CancellationToken cancellationToken = default);
}
/// <summary>Reads monitoring samples for servers.</summary>
public interface ISampleSetsApi
{
Task<SampleSet?> GetLatestAsync(long serverId, string? dataInterval = null, CancellationToken cancellationToken = default);
Task<Page<SampleSet>> ListAsync(long serverId, PageRequest? page = null, string? dataInterval = null, DateTimeOffset? start = null, DateTimeOffset? endAt = null, CancellationToken cancellationToken = default);
}
/// <summary>Manages BinaryLane servers and their server-scoped resources.</summary>
public interface IServersApi
{
Task<Server> GetAsync(long serverId, CancellationToken cancellationToken = default);
Task<Page<Server>> ListAsync(PageRequest? page = null, CancellationToken cancellationToken = default);
IAsyncEnumerable<Server> ListAllAsync(PageRequest? page = null, CancellationToken cancellationToken = default);
Task<Server> CreateAsync(CreateServerRequest request, CancellationToken cancellationToken = default);
Task DeleteAsync(long serverId, string? reason = null, CancellationToken cancellationToken = default);
Task<Page<BinaryLaneAction>> ListActionsAsync(long serverId, PageRequest? page = null, CancellationToken cancellationToken = default);
Task<BinaryLaneAction> GetActionAsync(long serverId, long actionId, CancellationToken cancellationToken = default);
Task<ActionSubmission> SubmitActionAsync(long serverId, ServerAction request, CancellationToken cancellationToken = default);
Task<IReadOnlyList<AdvancedFirewallRule>> GetAdvancedFirewallRulesAsync(long serverId, CancellationToken cancellationToken = default);
Task<AvailableAdvancedServerFeatures> GetAvailableAdvancedFeaturesAsync(long serverId, CancellationToken cancellationToken = default);
Task<Page<Image>> ListBackupsAsync(long serverId, PageRequest? page = null, CancellationToken cancellationToken = default);
Task<BinaryLaneAction> CreateBackupAsync(long serverId, UploadImageRequest request, CancellationToken cancellationToken = default);
Task<Page<Kernel>> ListKernelsAsync(long serverId, PageRequest? page = null, CancellationToken cancellationToken = default);
Task<Page<Image>> ListSnapshotsAsync(long serverId, PageRequest? page = null, CancellationToken cancellationToken = default);
Task<IReadOnlyList<ThresholdAlert>> GetThresholdAlertsAsync(long serverId, CancellationToken cancellationToken = default);
Task<IReadOnlyList<long>> GetThresholdAlertServerIdsAsync(CancellationToken cancellationToken = default);
Task<Page<LicensedSoftware>> ListSoftwareAsync(long serverId, PageRequest? page = null, CancellationToken cancellationToken = default);
Task<UserData> GetUserDataAsync(long serverId, CancellationToken cancellationToken = default);
Task<ServerConsole> GetConsoleAsync(long serverId, CancellationToken cancellationToken = default);
}
/// <summary>Lists available server sizes.</summary>
public interface ISizesApi
{
Task<Page<Size>> ListAsync(PageRequest? page = null, CancellationToken cancellationToken = default);
IAsyncEnumerable<Size> ListAllAsync(PageRequest? page = null, CancellationToken cancellationToken = default);
}
/// <summary>Lists available licensed software and operating-system software.</summary>
public interface ISoftwareApi
{
Task<Page<Software>> ListAsync(PageRequest? page = null, CancellationToken cancellationToken = default);
IAsyncEnumerable<Software> ListAllAsync(PageRequest? page = null, CancellationToken cancellationToken = default);
Task<Software> GetAsync(long softwareId, CancellationToken cancellationToken = default);
Task<Page<Software>> ListOperatingSystemAsync(string operatingSystemIdOrSlug, PageRequest? page = null, CancellationToken cancellationToken = default);
}
/// <summary>Manages private virtual networks and their members.</summary>
public interface IVpcsApi
{
Task<Page<Vpc>> ListAsync(PageRequest? page = null, CancellationToken cancellationToken = default);
IAsyncEnumerable<Vpc> ListAllAsync(PageRequest? page = null, CancellationToken cancellationToken = default);
Task<Vpc> GetAsync(long vpcId, CancellationToken cancellationToken = default);
Task<Vpc> CreateAsync(CreateVpcRequest request, CancellationToken cancellationToken = default);
Task<Vpc> ReplaceAsync(long vpcId, UpdateVpcRequest request, CancellationToken cancellationToken = default);
Task<Vpc> UpdateAsync(long vpcId, PatchVpcRequest request, CancellationToken cancellationToken = default);
Task DeleteAsync(long vpcId, CancellationToken cancellationToken = default);
Task<Page<VpcMember>> ListMembersAsync(long vpcId, PageRequest? page = null, CancellationToken cancellationToken = default);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

+348
View File
@@ -0,0 +1,348 @@
{
"version": 2,
"dependencies": {
".NETStandard,Version=v2.0": {
"Microsoft.Bcl.AsyncInterfaces": {
"type": "Direct",
"requested": "[8.0.0, )",
"resolved": "8.0.0",
"contentHash": "3WA9q9yVqJp222P3x1wYIGDAkpjAku0TMUaaQV22g6L67AI0LdOIrVS7Ht2vJfLHGSPVuqN94vIr15qn+HEkHw==",
"dependencies": {
"System.Threading.Tasks.Extensions": "4.5.4"
}
},
"Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "Direct",
"requested": "[8.0.2, )",
"resolved": "8.0.2",
"contentHash": "3iE7UF7MQkCv1cxzCahz+Y/guQbTqieyxyaWKhrRO91itI9cOKO76OHeQDahqG4MmW5umr3CcCvGmK92lWNlbg==",
"dependencies": {
"Microsoft.Bcl.AsyncInterfaces": "8.0.0",
"System.Threading.Tasks.Extensions": "4.5.4"
}
},
"Microsoft.Extensions.Http": {
"type": "Direct",
"requested": "[8.0.1, )",
"resolved": "8.0.1",
"contentHash": "kDYeKJUzh0qeg/AI+nSr3ffthmXYQTEb0nS9qRC7YhSbbuN4M4NPbaB77AJwtkTnCV9XZ7qYj3dkZaNcyl73EA==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2",
"Microsoft.Extensions.Logging": "8.0.1",
"Microsoft.Extensions.Logging.Abstractions": "8.0.2",
"Microsoft.Extensions.Options": "8.0.2"
}
},
"Microsoft.Extensions.Options": {
"type": "Direct",
"requested": "[8.0.2, )",
"resolved": "8.0.2",
"contentHash": "dWGKvhFybsaZpGmzkGCbNNwBD1rVlWzrZKANLW/CcbFJpCEceMCGzT7zZwHOGBCbwM0SzBuceMj5HN1LKV1QqA==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
"Microsoft.Extensions.Primitives": "8.0.0",
"System.ComponentModel.Annotations": "5.0.0"
}
},
"Microsoft.SourceLink.GitHub": {
"type": "Direct",
"requested": "[8.0.0, )",
"resolved": "8.0.0",
"contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==",
"dependencies": {
"Microsoft.Build.Tasks.Git": "8.0.0",
"Microsoft.SourceLink.Common": "8.0.0"
}
},
"NETStandard.Library": {
"type": "Direct",
"requested": "[2.0.3, )",
"resolved": "2.0.3",
"contentHash": "st47PosZSHrjECdjeIzZQbzivYBJFv6P2nv4cj2ypdI204DO+vZ7l5raGMiX4eXMJ53RfOIg+/s4DHVZ54Nu2A==",
"dependencies": {
"Microsoft.NETCore.Platforms": "1.1.0"
}
},
"System.Text.Json": {
"type": "Direct",
"requested": "[8.0.5, )",
"resolved": "8.0.5",
"contentHash": "0f1B50Ss7rqxXiaBJyzUu9bWFOO2/zSlifZ/UNMdiIpDYe4cY4LQQicP4nirK1OS31I43rn062UIJ1Q9bpmHpg==",
"dependencies": {
"Microsoft.Bcl.AsyncInterfaces": "8.0.0",
"System.Buffers": "4.5.1",
"System.Memory": "4.5.5",
"System.Runtime.CompilerServices.Unsafe": "6.0.0",
"System.Text.Encodings.Web": "8.0.0",
"System.Threading.Tasks.Extensions": "4.5.4"
}
},
"Microsoft.Build.Tasks.Git": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ=="
},
"Microsoft.Extensions.Configuration.Abstractions": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "3lE/iLSutpgX1CC0NOW70FJoGARRHbyKmG7dc0klnUZ9Dd9hS6N/POPWhKhMLCEuNN5nXEY5agmlFtH562vqhQ==",
"dependencies": {
"Microsoft.Extensions.Primitives": "8.0.0"
}
},
"Microsoft.Extensions.Logging": {
"type": "Transitive",
"resolved": "8.0.1",
"contentHash": "4x+pzsQEbqxhNf1QYRr5TDkLP9UsLT3A6MdRKDDEgrW7h1ljiEPgTNhKYUhNCCAaVpQECVQ+onA91PTPnIp6Lw==",
"dependencies": {
"Microsoft.Bcl.AsyncInterfaces": "8.0.0",
"Microsoft.Extensions.DependencyInjection": "8.0.1",
"Microsoft.Extensions.Logging.Abstractions": "8.0.2",
"Microsoft.Extensions.Options": "8.0.2",
"System.Diagnostics.DiagnosticSource": "8.0.1"
}
},
"Microsoft.Extensions.Logging.Abstractions": {
"type": "Transitive",
"resolved": "8.0.2",
"contentHash": "nroMDjS7hNBPtkZqVBbSiQaQjWRDxITI8Y7XnDs97rqG3EbzVTNLZQf7bIeUJcaHOV8bca47s1Uxq94+2oGdxA==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2",
"System.Buffers": "4.5.1",
"System.Diagnostics.DiagnosticSource": "8.0.1",
"System.Memory": "4.5.5"
}
},
"Microsoft.Extensions.Primitives": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g==",
"dependencies": {
"System.Memory": "4.5.5",
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
}
},
"Microsoft.NETCore.Platforms": {
"type": "Transitive",
"resolved": "1.1.0",
"contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A=="
},
"Microsoft.SourceLink.Common": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw=="
},
"System.Buffers": {
"type": "Transitive",
"resolved": "4.5.1",
"contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg=="
},
"System.ComponentModel.Annotations": {
"type": "Transitive",
"resolved": "5.0.0",
"contentHash": "dMkqfy2el8A8/I76n2Hi1oBFEbG1SfxD2l5nhwXV3XjlnOmwxJlQbYpJH4W51odnU9sARCSAgv7S3CyAFMkpYg=="
},
"System.Diagnostics.DiagnosticSource": {
"type": "Transitive",
"resolved": "8.0.1",
"contentHash": "vaoWjvkG1aenR2XdjaVivlCV9fADfgyhW5bZtXT23qaEea0lWiUljdQuze4E31vKM7ZWJaSUsbYIKE3rnzfZUg==",
"dependencies": {
"System.Memory": "4.5.5",
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
}
},
"System.Memory": {
"type": "Transitive",
"resolved": "4.5.5",
"contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==",
"dependencies": {
"System.Buffers": "4.5.1",
"System.Numerics.Vectors": "4.4.0",
"System.Runtime.CompilerServices.Unsafe": "4.5.3"
}
},
"System.Numerics.Vectors": {
"type": "Transitive",
"resolved": "4.4.0",
"contentHash": "UiLzLW+Lw6HLed1Hcg+8jSRttrbuXv7DANVj0DkL9g6EnnzbL75EB7EWsw5uRbhxd/4YdG8li5XizGWepmG3PQ=="
},
"System.Runtime.CompilerServices.Unsafe": {
"type": "Transitive",
"resolved": "6.0.0",
"contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg=="
},
"System.Text.Encodings.Web": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ==",
"dependencies": {
"System.Buffers": "4.5.1",
"System.Memory": "4.5.5",
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
}
},
"System.Threading.Tasks.Extensions": {
"type": "Transitive",
"resolved": "4.5.4",
"contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg==",
"dependencies": {
"System.Runtime.CompilerServices.Unsafe": "4.5.3"
}
},
"Microsoft.Extensions.DependencyInjection": {
"type": "CentralTransitive",
"requested": "[8.0.1, )",
"resolved": "8.0.1",
"contentHash": "BmANAnR5Xd4Oqw7yQ75xOAYODybZQRzdeNucg7kS5wWKd2PNnMdYtJ2Vciy0QLylRmv42DGl5+AFL9izA6F1Rw==",
"dependencies": {
"Microsoft.Bcl.AsyncInterfaces": "8.0.0",
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2",
"System.Threading.Tasks.Extensions": "4.5.4"
}
}
},
"net8.0": {
"Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "Direct",
"requested": "[8.0.2, )",
"resolved": "8.0.2",
"contentHash": "3iE7UF7MQkCv1cxzCahz+Y/guQbTqieyxyaWKhrRO91itI9cOKO76OHeQDahqG4MmW5umr3CcCvGmK92lWNlbg=="
},
"Microsoft.Extensions.Http": {
"type": "Direct",
"requested": "[8.0.1, )",
"resolved": "8.0.1",
"contentHash": "kDYeKJUzh0qeg/AI+nSr3ffthmXYQTEb0nS9qRC7YhSbbuN4M4NPbaB77AJwtkTnCV9XZ7qYj3dkZaNcyl73EA==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2",
"Microsoft.Extensions.Diagnostics": "8.0.1",
"Microsoft.Extensions.Logging": "8.0.1",
"Microsoft.Extensions.Logging.Abstractions": "8.0.2",
"Microsoft.Extensions.Options": "8.0.2"
}
},
"Microsoft.Extensions.Options": {
"type": "Direct",
"requested": "[8.0.2, )",
"resolved": "8.0.2",
"contentHash": "dWGKvhFybsaZpGmzkGCbNNwBD1rVlWzrZKANLW/CcbFJpCEceMCGzT7zZwHOGBCbwM0SzBuceMj5HN1LKV1QqA==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
"Microsoft.Extensions.Primitives": "8.0.0"
}
},
"Microsoft.SourceLink.GitHub": {
"type": "Direct",
"requested": "[8.0.0, )",
"resolved": "8.0.0",
"contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==",
"dependencies": {
"Microsoft.Build.Tasks.Git": "8.0.0",
"Microsoft.SourceLink.Common": "8.0.0"
}
},
"Microsoft.Build.Tasks.Git": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ=="
},
"Microsoft.Extensions.Configuration": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "0J/9YNXTMWSZP2p2+nvl8p71zpSwokZXZuJW+VjdErkegAnFdO1XlqtA62SJtgVYHdKu3uPxJHcMR/r35HwFBA==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
"Microsoft.Extensions.Primitives": "8.0.0"
}
},
"Microsoft.Extensions.Configuration.Abstractions": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "3lE/iLSutpgX1CC0NOW70FJoGARRHbyKmG7dc0klnUZ9Dd9hS6N/POPWhKhMLCEuNN5nXEY5agmlFtH562vqhQ==",
"dependencies": {
"Microsoft.Extensions.Primitives": "8.0.0"
}
},
"Microsoft.Extensions.Diagnostics": {
"type": "Transitive",
"resolved": "8.0.1",
"contentHash": "doVPCUUCY7c6LhBsEfiy3W1bvS7Mi6LkfQMS8nlC22jZWNxBv8VO8bdfeyvpYFst6Kxqk7HBC6lytmEoBssvSQ==",
"dependencies": {
"Microsoft.Extensions.Configuration": "8.0.0",
"Microsoft.Extensions.Diagnostics.Abstractions": "8.0.1",
"Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0"
}
},
"Microsoft.Extensions.Diagnostics.Abstractions": {
"type": "Transitive",
"resolved": "8.0.1",
"contentHash": "elH2vmwNmsXuKmUeMQ4YW9ldXiF+gSGDgg1vORksob5POnpaI6caj1Hu8zaYbEuibhqCoWg0YRWDazBY3zjBfg==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2",
"Microsoft.Extensions.Options": "8.0.2"
}
},
"Microsoft.Extensions.Logging": {
"type": "Transitive",
"resolved": "8.0.1",
"contentHash": "4x+pzsQEbqxhNf1QYRr5TDkLP9UsLT3A6MdRKDDEgrW7h1ljiEPgTNhKYUhNCCAaVpQECVQ+onA91PTPnIp6Lw==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "8.0.1",
"Microsoft.Extensions.Logging.Abstractions": "8.0.2",
"Microsoft.Extensions.Options": "8.0.2"
}
},
"Microsoft.Extensions.Logging.Abstractions": {
"type": "Transitive",
"resolved": "8.0.2",
"contentHash": "nroMDjS7hNBPtkZqVBbSiQaQjWRDxITI8Y7XnDs97rqG3EbzVTNLZQf7bIeUJcaHOV8bca47s1Uxq94+2oGdxA==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2"
}
},
"Microsoft.Extensions.Primitives": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g=="
},
"Microsoft.SourceLink.Common": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw=="
},
"Microsoft.Extensions.Configuration.Binder": {
"type": "CentralTransitive",
"requested": "[8.0.2, )",
"resolved": "8.0.0",
"contentHash": "mBMoXLsr5s1y2zOHWmKsE9veDcx8h1x/c3rz4baEdQKTeDcmQAPNbB54Pi/lhFO3K431eEq6PFbMgLaa6PHFfA==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0"
}
},
"Microsoft.Extensions.DependencyInjection": {
"type": "CentralTransitive",
"requested": "[8.0.1, )",
"resolved": "8.0.1",
"contentHash": "BmANAnR5Xd4Oqw7yQ75xOAYODybZQRzdeNucg7kS5wWKd2PNnMdYtJ2Vciy0QLylRmv42DGl5+AFL9izA6F1Rw==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2"
}
},
"Microsoft.Extensions.Options.ConfigurationExtensions": {
"type": "CentralTransitive",
"requested": "[8.0.0, )",
"resolved": "8.0.0",
"contentHash": "0f4DMRqEd50zQh+UyJc+/HiBsZ3vhAQALgdkcQEalSH1L2isdC7Yj54M3cyo5e+BeO5fcBQ7Dxly8XiBBcvRgw==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
"Microsoft.Extensions.Configuration.Binder": "8.0.0",
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
"Microsoft.Extensions.Options": "8.0.0",
"Microsoft.Extensions.Primitives": "8.0.0"
}
}
}
}
}
@@ -0,0 +1,3 @@
using Xunit;
[assembly: CollectionBehavior(DisableTestParallelization = true)]
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" PrivateAssets="all" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../src/BinaryLane.Api/BinaryLane.Api.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,375 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using BinaryLane.Api.V2.Authentication;
using BinaryLane.Api.V2.Errors;
using BinaryLane.Api.V2.Http;
using Xunit;
namespace BinaryLane.Api.Tests;
public sealed class BinaryLaneHttpExecutorTests
{
[Fact]
public async Task GetAsyncAddsBearerHeaderAndSerializesQueryValues()
{
HttpRequestMessage? received = null;
using var httpClient = new HttpClient(new HttpClientTestHandler((request, _) =>
{
received = request;
return Task.FromResult(JsonResponse(HttpStatusCode.OK, "{\"value\":\"ok\"}"));
}))
{
BaseAddress = new Uri("https://api.example.test/"),
};
var executor = new BinaryLaneHttpExecutor(httpClient, new StaticBinaryLaneTokenProvider("test-token"));
var response = await executor.GetAsync<JsonElement>(
"v2/servers",
new System.Collections.Generic.Dictionary<string, object?>
{
["page"] = 2,
["include_archived"] = false,
});
Assert.Equal("ok", response.GetProperty("value").GetString());
Assert.NotNull(received);
Assert.Equal("Bearer", received!.Headers.Authorization!.Scheme);
Assert.Equal("test-token", received.Headers.Authorization.Parameter);
Assert.Equal("?page=2&include_archived=false", received.RequestUri!.Query);
}
[Fact]
public async Task GetAsyncMapsUnauthorizedResponseWithoutLeakingBearerToken()
{
using var httpClient = new HttpClient(new HttpClientTestHandler((_, _) =>
Task.FromResult(JsonResponse(HttpStatusCode.Unauthorized, "{\"title\":\"Unauthorized\",\"detail\":\"Invalid token\"}"))))
{
BaseAddress = new Uri("https://api.example.test/"),
};
var executor = new BinaryLaneHttpExecutor(httpClient, new StaticBinaryLaneTokenProvider("super-secret-token"));
var exception = await Assert.ThrowsAsync<BinaryLaneUnauthorizedException>(
() => executor.GetAsync<JsonElement>("v2/account"));
Assert.Equal(HttpStatusCode.Unauthorized, exception.StatusCode);
Assert.Equal("Invalid token", exception.Problem!.Detail);
Assert.DoesNotContain("Invalid token", exception.Message, StringComparison.Ordinal);
Assert.DoesNotContain("super-secret-token", exception.ToString(), StringComparison.Ordinal);
Assert.DoesNotContain("super-secret-token", exception.ResponseBody ?? string.Empty, StringComparison.Ordinal);
}
[Fact]
public async Task GetAsyncPropagatesCancellationToTheHttpHandler()
{
using var cancellation = new CancellationTokenSource();
using var httpClient = new HttpClient(new HttpClientTestHandler(async (_, token) =>
{
await Task.Delay(Timeout.InfiniteTimeSpan, token);
throw new InvalidOperationException("Unreachable.");
}))
{
BaseAddress = new Uri("https://api.example.test/"),
};
var executor = new BinaryLaneHttpExecutor(httpClient, new StaticBinaryLaneTokenProvider("token"));
cancellation.Cancel();
await Assert.ThrowsAnyAsync<OperationCanceledException>(
() => executor.GetAsync<JsonElement>("v2/account", cancellationToken: cancellation.Token));
}
[Fact]
public async Task TokenProviderIsEvaluatedForEveryRequestSoTokensCanRotate()
{
var authorizationTokens = new List<string?>();
using var httpClient = new HttpClient(new HttpClientTestHandler((request, _) =>
{
authorizationTokens.Add(request.Headers.Authorization?.Parameter);
return Task.FromResult(JsonResponse(HttpStatusCode.OK, "{}"));
}))
{
BaseAddress = new Uri("https://api.example.test/"),
};
var executor = new BinaryLaneHttpExecutor(
httpClient,
new RotatingTokenProvider("first-token", "second-token"));
await executor.GetAsync<JsonElement>("v2/account");
await executor.GetAsync<JsonElement>("v2/account");
Assert.Collection(
authorizationTokens,
token => Assert.Equal("first-token", token),
token => Assert.Equal("second-token", token));
}
[Fact]
public async Task QueryFormattingOmitsNullsAndUsesInvariantWireValues()
{
HttpRequestMessage? received = null;
using var httpClient = new HttpClient(new HttpClientTestHandler((request, _) =>
{
received = request;
return Task.FromResult(JsonResponse(HttpStatusCode.OK, "{}"));
}))
{
BaseAddress = new Uri("https://api.example.test/"),
};
var executor = new BinaryLaneHttpExecutor(httpClient, new StaticBinaryLaneTokenProvider("token"));
var timestamp = new DateTimeOffset(2026, 8, 16, 12, 34, 56, TimeSpan.Zero);
await executor.GetAsync<JsonElement>(
"v2/actions?existing=true",
new Dictionary<string, object?>
{
["after"] = timestamp,
["amount"] = 10.5m,
["omit_me"] = null,
});
Assert.NotNull(received);
var query = received!.RequestUri!.Query;
Assert.Contains("existing=true", query, StringComparison.Ordinal);
Assert.Contains("after=2026-08-16T12%3A34%3A56.0000000%2B00%3A00", query, StringComparison.Ordinal);
Assert.Contains("amount=10.5", query, StringComparison.Ordinal);
Assert.DoesNotContain("omit_me", query, StringComparison.Ordinal);
}
[Fact]
public async Task ValidationErrorsMapToValidationExceptionAndPreserveProblemDetails()
{
using var httpClient = new HttpClient(new HttpClientTestHandler((_, _) =>
Task.FromResult(JsonResponse(
(HttpStatusCode)422,
"{\"title\":\"Validation failed\",\"detail\":\"region is required\",\"status\":422}"))))
{
BaseAddress = new Uri("https://api.example.test/"),
};
var executor = new BinaryLaneHttpExecutor(httpClient, new StaticBinaryLaneTokenProvider("token"));
var exception = await Assert.ThrowsAsync<BinaryLaneValidationException>(
() => executor.PostAsync<JsonElement>("v2/servers", new { }));
Assert.Equal((HttpStatusCode)422, exception.StatusCode);
Assert.Equal("Validation failed", exception.Problem!.Title);
Assert.Equal("region is required", exception.Problem.Detail);
}
[Fact]
public async Task MutationsAreNotRetriedImplicitly()
{
var requestCount = 0;
using var httpClient = new HttpClient(new HttpClientTestHandler((_, _) =>
{
requestCount++;
return Task.FromResult(JsonResponse(HttpStatusCode.InternalServerError, "{\"title\":\"Failure\"}"));
}))
{
BaseAddress = new Uri("https://api.example.test/"),
};
var executor = new BinaryLaneHttpExecutor(httpClient, new StaticBinaryLaneTokenProvider("token"));
await Assert.ThrowsAsync<BinaryLaneApiException>(
() => executor.PostAsync<JsonElement>("v2/servers", new { name = "must-not-retry" }));
Assert.Equal(1, requestCount);
}
[Fact]
public async Task EmptyTokenFailsBeforeTheRequestIsDispatched()
{
var requestCount = 0;
using var httpClient = new HttpClient(new HttpClientTestHandler((_, _) =>
{
requestCount++;
return Task.FromResult(JsonResponse(HttpStatusCode.OK, "{}"));
}))
{
BaseAddress = new Uri("https://api.example.test/"),
};
var executor = new BinaryLaneHttpExecutor(httpClient, new RotatingTokenProvider(string.Empty));
await Assert.ThrowsAsync<InvalidOperationException>(
() => executor.GetAsync<JsonElement>("v2/account"));
Assert.Equal(0, requestCount);
}
[Fact]
public async Task AbsoluteUrlsOutsideTheConfiguredOriginDoNotReceiveTheBearerToken()
{
var requestCount = 0;
using var httpClient = new HttpClient(new HttpClientTestHandler((_, _) =>
{
requestCount++;
return Task.FromResult(JsonResponse(HttpStatusCode.OK, "{}"));
}))
{
BaseAddress = new Uri("https://api.example.test/"),
};
var executor = new BinaryLaneHttpExecutor(httpClient, new StaticBinaryLaneTokenProvider("token"));
await Assert.ThrowsAsync<ArgumentException>(
() => executor.GetAsync<JsonElement>("https://other.example.test/v2/servers"));
Assert.Equal(0, requestCount);
}
[Fact]
public async Task NonHttpAbsoluteUrlsAreRejectedBeforeTheRequestIsDispatched()
{
var requestCount = 0;
using var httpClient = new HttpClient(new HttpClientTestHandler((_, _) =>
{
requestCount++;
return Task.FromResult(JsonResponse(HttpStatusCode.OK, "{}"));
}))
{
BaseAddress = new Uri("https://api.example.test/"),
};
var executor = new BinaryLaneHttpExecutor(httpClient, new StaticBinaryLaneTokenProvider("token"));
await Assert.ThrowsAsync<ArgumentException>(
() => executor.GetAsync<JsonElement>("ftp://other.example.test/v2/servers"));
Assert.Equal(0, requestCount);
}
[Fact]
public async Task CallerCannotOverrideTheTokenProviderAuthorizationHeader()
{
var requestCount = 0;
using var httpClient = new HttpClient(new HttpClientTestHandler((_, _) =>
{
requestCount++;
return Task.FromResult(JsonResponse(HttpStatusCode.OK, "{}"));
}))
{
BaseAddress = new Uri("https://api.example.test/"),
};
var executor = new BinaryLaneHttpExecutor(httpClient, new StaticBinaryLaneTokenProvider("token"));
var request = new BinaryLaneRequest(HttpMethod.Get, "v2/account")
{
Headers = new Dictionary<string, string>
{
["Authorization"] = "Bearer caller-token",
},
};
await Assert.ThrowsAsync<ArgumentException>(() => executor.SendAsync<JsonElement>(request));
Assert.Equal(0, requestCount);
}
[Fact]
public async Task CallerCannotOverrideTheRequestAuthorityWithAHostHeader()
{
var requestCount = 0;
using var httpClient = new HttpClient(new HttpClientTestHandler((_, _) =>
{
requestCount++;
return Task.FromResult(JsonResponse(HttpStatusCode.OK, "{}"));
}))
{
BaseAddress = new Uri("https://api.example.test/"),
};
var executor = new BinaryLaneHttpExecutor(httpClient, new StaticBinaryLaneTokenProvider("token"));
var request = new BinaryLaneRequest(HttpMethod.Get, "v2/account")
{
Headers = new Dictionary<string, string>
{
["Host"] = "other.example.test",
},
};
await Assert.ThrowsAsync<ArgumentException>(() => executor.SendAsync<JsonElement>(request));
Assert.Equal(0, requestCount);
}
[Fact]
public async Task PlaintextBaseAddressesAreRejectedBeforeTheRequestIsDispatched()
{
var requestCount = 0;
using var httpClient = new HttpClient(new HttpClientTestHandler((_, _) =>
{
requestCount++;
return Task.FromResult(JsonResponse(HttpStatusCode.OK, "{}"));
}))
{
BaseAddress = new Uri("http://api.example.test/"),
};
var executor = new BinaryLaneHttpExecutor(httpClient, new StaticBinaryLaneTokenProvider("token"));
await Assert.ThrowsAsync<InvalidOperationException>(
() => executor.GetAsync<JsonElement>("v2/account"));
Assert.Equal(0, requestCount);
}
[Fact]
public async Task SuccessResponseBodiesLargerThanTheLimitAreRejectedBeforeBuffering()
{
using var httpClient = new HttpClient(new HttpClientTestHandler((_, _) =>
{
var content = new ByteArrayContent(Array.Empty<byte>());
content.Headers.ContentLength = (16L * 1024 * 1024) + 1;
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { Content = content });
}))
{
BaseAddress = new Uri("https://api.example.test/"),
};
var executor = new BinaryLaneHttpExecutor(httpClient, new StaticBinaryLaneTokenProvider("token"));
var exception = await Assert.ThrowsAsync<HttpRequestException>(
() => executor.GetAsync<JsonElement>("v2/account"));
Assert.Contains("exceeded", exception.Message, StringComparison.Ordinal);
}
[Fact]
public async Task ErrorResponseBodyIsBoundedBeforeItIsStoredOnTheException()
{
var responseBody = "{\"detail\":\"" + new string('x', 40_000) + "\"}";
using var httpClient = new HttpClient(new HttpClientTestHandler((_, _) =>
Task.FromResult(JsonResponse(HttpStatusCode.InternalServerError, responseBody))))
{
BaseAddress = new Uri("https://api.example.test/"),
};
var executor = new BinaryLaneHttpExecutor(httpClient, new StaticBinaryLaneTokenProvider("token"));
var exception = await Assert.ThrowsAsync<BinaryLaneApiException>(
() => executor.GetAsync<JsonElement>("v2/account"));
Assert.NotNull(exception.ResponseBody);
Assert.Equal(32_769, exception.ResponseBody!.Length);
Assert.EndsWith("…", exception.ResponseBody, StringComparison.Ordinal);
}
private static HttpResponseMessage JsonResponse(HttpStatusCode statusCode, string json) =>
new(statusCode)
{
Content = new StringContent(json),
};
private sealed class RotatingTokenProvider : IBinaryLaneTokenProvider
{
private readonly Queue<string> _tokens;
public RotatingTokenProvider(params string[] tokens)
{
_tokens = new Queue<string>(tokens);
}
public ValueTask<string> GetTokenAsync(CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
return new ValueTask<string>(_tokens.Dequeue());
}
}
}
@@ -0,0 +1,157 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
using BinaryLane.Api.V2;
using BinaryLane.Api.V2.Authentication;
using BinaryLane.Api.V2.Models;
using Xunit;
namespace BinaryLane.Api.Tests;
public sealed class ClientResourceTests
{
[Fact]
public async Task ServersListAllFollowsTheProviderNextPageLink()
{
var requestCount = 0;
using var httpClient = new HttpClient(new HttpClientTestHandler((request, _) =>
{
requestCount++;
var json = request.RequestUri!.Query.Contains("page=2", StringComparison.Ordinal)
? "{\"meta\":{\"total\":2},\"links\":{\"pages\":{}},\"servers\":[{\"id\":2,\"name\":\"two\",\"status\":\"active\"}]}"
: "{\"meta\":{\"total\":2},\"links\":{\"pages\":{\"next\":\"/v2/servers?page=2\"}},\"servers\":[{\"id\":1,\"name\":\"one\",\"status\":\"active\"}]}";
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(json) });
}))
{
BaseAddress = new Uri("https://api.example.test/"),
};
var client = new BinaryLaneClient(httpClient, new StaticBinaryLaneTokenProvider("token"));
var servers = new List<Server>();
await foreach (var server in client.Servers.ListAllAsync())
{
servers.Add(server);
}
Assert.Equal(2, requestCount);
Assert.Collection(
servers,
server => Assert.Equal("one", server.Name),
server => Assert.Equal("two", server.Name));
}
[Fact]
public async Task ServersListAllStopsWhenTheProviderRepeatsANextPageLink()
{
var requestCount = 0;
using var httpClient = new HttpClient(new HttpClientTestHandler((_, _) =>
{
requestCount++;
const string json = "{\"meta\":{\"total\":1},\"links\":{\"pages\":{\"next\":\"/v2/servers?page=1\"}},\"servers\":[{\"id\":1,\"name\":\"one\",\"status\":\"active\"}]}";
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(json) });
}))
{
BaseAddress = new Uri("https://api.example.test/"),
};
var client = new BinaryLaneClient(httpClient, new StaticBinaryLaneTokenProvider("token"));
var exception = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
{
await foreach (var _ in client.Servers.ListAllAsync())
{
}
});
Assert.Contains("repeated pagination link", exception.Message, StringComparison.Ordinal);
Assert.Equal(2, requestCount);
}
[Fact]
public async Task SubmitActionUsesRealActionsEndpointAndReportsAcceptedStatus()
{
HttpRequestMessage? received = null;
string? body = null;
using var httpClient = new HttpClient(new HttpClientTestHandler(async (request, cancellationToken) =>
{
received = request;
body = request.Content is null ? null : await request.Content.ReadAsStringAsync(cancellationToken);
return new HttpResponseMessage(HttpStatusCode.Accepted)
{
Content = new StringContent("{\"action\":{\"id\":9,\"type\":\"power_on\",\"status\":\"in-progress\",\"started_at\":\"2026-01-01T00:00:00Z\"}}"),
};
}))
{
BaseAddress = new Uri("https://api.example.test/"),
};
var client = new BinaryLaneClient(httpClient, new StaticBinaryLaneTokenProvider("token"));
var submission = await client.Servers.SubmitActionAsync(123, new PowerOnServerAction());
Assert.NotNull(received);
Assert.Equal("/v2/servers/123/actions", received!.RequestUri!.AbsolutePath);
Assert.Equal("POST", received.Method.Method);
Assert.Contains("\"type\":\"power_on\"", body!, StringComparison.Ordinal);
Assert.True(submission.IsAccepted);
Assert.Equal(9, submission.Action!.Id);
}
[Fact]
public async Task ListEndpointsRejectAResponseWithoutTheExpectedCollectionEnvelope()
{
using var httpClient = new HttpClient(new HttpClientTestHandler((_, _) =>
Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent("{\"meta\":{\"total\":0},\"wrong_collection\":[]}"),
})))
{
BaseAddress = new Uri("https://api.example.test/"),
};
var client = new BinaryLaneClient(httpClient, new StaticBinaryLaneTokenProvider("token"));
var exception = await Assert.ThrowsAsync<JsonException>(() => client.Servers.ListAsync());
Assert.Contains("servers", exception.Message, StringComparison.Ordinal);
}
[Fact]
public async Task UserDataUsesTheDocumentedDirectResponseBody()
{
using var httpClient = new HttpClient(new HttpClientTestHandler((request, _) =>
{
Assert.Equal("/v2/servers/123/user_data", request.RequestUri!.AbsolutePath);
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent("{\"user_data\":\"#cloud-config\"}"),
});
}))
{
BaseAddress = new Uri("https://api.example.test/"),
};
var client = new BinaryLaneClient(httpClient, new StaticBinaryLaneTokenProvider("token"));
var userData = await client.Servers.GetUserDataAsync(123);
Assert.Equal("#cloud-config", userData.Value);
}
[Fact]
public async Task ListEndpointsRejectNullItemsRatherThanSilentlyDroppingThem()
{
using var httpClient = new HttpClient(new HttpClientTestHandler((_, _) =>
Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent("{\"meta\":{\"total\":1},\"servers\":[null]}"),
})))
{
BaseAddress = new Uri("https://api.example.test/"),
};
var client = new BinaryLaneClient(httpClient, new StaticBinaryLaneTokenProvider("token"));
var exception = await Assert.ThrowsAsync<JsonException>(() => client.Servers.ListAsync());
Assert.Contains("servers", exception.Message, StringComparison.Ordinal);
}
}
@@ -0,0 +1,85 @@
using System;
using BinaryLane.Api.V2;
using BinaryLane.Api.V2.Configuration;
using BinaryLane.Api.V2.DependencyInjection;
using BinaryLane.Api.V2.Resources;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Xunit;
namespace BinaryLane.Api.Tests;
public sealed class DependencyInjectionTests
{
[Fact]
public void AddBinaryLaneApiRegistersAComposableClient()
{
var services = new ServiceCollection();
services.AddBinaryLaneApi(options => options.ApiToken = "test-token");
using var provider = services.BuildServiceProvider();
var client = provider.GetRequiredService<IBinaryLaneClient>();
Assert.NotNull(client.Account);
Assert.NotNull(client.Actions);
Assert.NotNull(client.Servers);
Assert.NotNull(client.Regions);
Assert.NotNull(client.Vpcs);
Assert.NotNull(provider.GetRequiredService<IServersApi>());
Assert.NotNull(provider.GetRequiredService<IDomainsApi>());
}
[Fact]
public void InvalidOptionsAreRejectedWhenResolved()
{
var services = new ServiceCollection();
services.AddBinaryLaneApi(options =>
{
options.BaseUrl = "not-a-url";
options.ApiToken = "test-token";
});
using var provider = services.BuildServiceProvider();
Assert.Throws<OptionsValidationException>(
() => provider.GetRequiredService<IOptions<BinaryLaneOptions>>().Value);
}
[Theory]
[InlineData(0)]
[InlineData(301)]
public void OutOfRangeTimeoutIsRejectedWhenResolved(int timeoutSeconds)
{
var services = new ServiceCollection();
services.AddBinaryLaneApi(options =>
{
options.ApiToken = "test-token";
options.RequestTimeoutSeconds = timeoutSeconds;
});
using var provider = services.BuildServiceProvider();
Assert.Throws<OptionsValidationException>(
() => provider.GetRequiredService<IOptions<BinaryLaneOptions>>().Value);
}
[Theory]
[InlineData("http://api.example.test/")]
[InlineData("https://user:password@api.example.test/")]
[InlineData("https://api.example.test/?unexpected=value")]
[InlineData("https://api.example.test/#fragment")]
public void BaseUrlWithCredentialsQueryOrFragmentIsRejected(string baseUrl)
{
var services = new ServiceCollection();
services.AddBinaryLaneApi(options =>
{
options.BaseUrl = baseUrl;
options.ApiToken = "test-token";
});
using var provider = services.BuildServiceProvider();
Assert.Throws<OptionsValidationException>(
() => provider.GetRequiredService<IOptions<BinaryLaneOptions>>().Value);
}
}
@@ -0,0 +1,17 @@
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace BinaryLane.Api.Tests;
internal sealed class HttpClientTestHandler : HttpMessageHandler
{
private readonly Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> _send;
public HttpClientTestHandler(Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> send) =>
_send = send ?? throw new ArgumentNullException(nameof(send));
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) =>
_send(request, cancellationToken);
}
@@ -0,0 +1,149 @@
using System.Text.Json;
using BinaryLane.Api.V2.Http;
using BinaryLane.Api.V2.Models;
using Xunit;
namespace BinaryLane.Api.Tests;
public sealed class ModelSerializationTests
{
private static readonly JsonSerializerOptions Json =
new BinaryLaneJsonSerializerOptions().SerializerOptions;
[Fact]
public void PowerOnActionSerializesTheProviderDiscriminator()
{
string json = JsonSerializer.Serialize<ServerAction>(new PowerOnServerAction(), Json);
using JsonDocument document = JsonDocument.Parse(json);
Assert.Equal("power_on", document.RootElement.GetProperty("type").GetString());
Assert.Single(document.RootElement.EnumerateObject());
}
[Fact]
public void CreateServerRequestSerializesBinaryLaneWireNamesAndOmitsNulls()
{
CreateServerRequest request = new()
{
Name = "demo-server",
Image = "ubuntu-24.04",
Region = "melbourne",
Size = "std-min",
PortBlocking = false,
};
string json = JsonSerializer.Serialize(request, Json);
using JsonDocument document = JsonDocument.Parse(json);
Assert.Equal("demo-server", document.RootElement.GetProperty("name").GetString());
Assert.Equal("ubuntu-24.04", document.RootElement.GetProperty("image").GetString());
Assert.Equal("melbourne", document.RootElement.GetProperty("region").GetString());
Assert.Equal("std-min", document.RootElement.GetProperty("size").GetString());
Assert.False(document.RootElement.GetProperty("port_blocking").GetBoolean());
Assert.False(document.RootElement.TryGetProperty("password", out _));
Assert.False(document.RootElement.TryGetProperty("user_data", out _));
}
[Fact]
public void UnknownResponsePropertiesAreRetainedForForwardCompatibility()
{
const string json = """
{
"email": "alex@example.test",
"email_verified": true,
"two_factor_authentication_enabled": false,
"status": "active",
"tax_code": { "name": "GST", "type": "scalar" },
"configured_payment_methods": ["credit-card"],
"additional_ipv4_limit": 2,
"future_provider_field": "kept"
}
""";
Account? account = JsonSerializer.Deserialize<Account>(json, Json);
Assert.NotNull(account);
Assert.Equal("active", account.Status);
Assert.NotNull(account.AdditionalProperties);
Assert.Equal(
"kept",
account.AdditionalProperties!["future_provider_field"].GetString());
}
[Fact]
public void PreviewEnumValuesRemainRawStrings()
{
const string json = """
{
"id": 42,
"status": "future-status",
"type": "future-action",
"started_at": "2026-08-16T12:34:56Z",
"title": "Future action",
"reason": "A future provider action",
"progress": { "percent_complete": 0, "completed_steps": [] }
}
""";
BinaryLaneAction? action = JsonSerializer.Deserialize<BinaryLaneAction>(json, Json);
Assert.NotNull(action);
Assert.Equal("future-status", action.Status);
Assert.Equal("future-action", action.Type);
}
[Fact]
public void DocumentedValuesAreAvailableWithoutClosedEnums()
{
Assert.Equal("completed", BinaryLaneValues.ActionStatus.Completed);
Assert.Equal("power_on", BinaryLaneValues.ServerActionType.PowerOn);
Assert.Equal("AAAA", BinaryLaneValues.DomainRecordType.Aaaa);
}
[Fact]
public void ComplexServerActionUsesTheSharedEndpointDiscriminatorAndWireNames()
{
ServerAction action = new ChangeVpcIpv4ServerAction
{
CurrentIpv4Address = "10.0.0.10",
NewIpv4Address = "10.0.0.20",
};
string json = JsonSerializer.Serialize(action, Json);
using JsonDocument document = JsonDocument.Parse(json);
Assert.Equal("change_vpc_ipv4", document.RootElement.GetProperty("type").GetString());
Assert.Equal("10.0.0.10", document.RootElement.GetProperty("current_ipv4_address").GetString());
Assert.Equal("10.0.0.20", document.RootElement.GetProperty("new_ipv4_address").GetString());
}
[Fact]
public void ServerActionConverterDeserializesKnownAndUnknownProviderActions()
{
const string knownJson = """
{
"type": "change_vpc_ipv4",
"current_ipv4_address": "10.0.0.10",
"new_ipv4_address": "10.0.0.20"
}
""";
const string unknownJson = """
{
"type": "future_action",
"provider_option": "kept"
}
""";
ServerAction? known = JsonSerializer.Deserialize<ServerAction>(knownJson, Json);
ServerAction? unknown = JsonSerializer.Deserialize<ServerAction>(unknownJson, Json);
ChangeVpcIpv4ServerAction typed = Assert.IsType<ChangeVpcIpv4ServerAction>(known);
Assert.Equal("10.0.0.10", typed.CurrentIpv4Address);
Assert.Equal("10.0.0.20", typed.NewIpv4Address);
UnknownServerAction future = Assert.IsType<UnknownServerAction>(unknown);
Assert.Equal("future_action", future.Type);
Assert.NotNull(future.AdditionalProperties);
Assert.Equal("kept", future.AdditionalProperties!["provider_option"].GetString());
}
}
@@ -0,0 +1,240 @@
{
"version": 2,
"dependencies": {
"net8.0": {
"coverlet.collector": {
"type": "Direct",
"requested": "[6.0.2, )",
"resolved": "6.0.2",
"contentHash": "bJShQ6uWRTQ100ZeyiMqcFlhP7WJ+bCuabUs885dJiBEzMsJMSFr7BOyeCw4rgvQokteGi5rKQTlkhfQPUXg2A=="
},
"Microsoft.Extensions.DependencyInjection": {
"type": "Direct",
"requested": "[8.0.1, )",
"resolved": "8.0.1",
"contentHash": "BmANAnR5Xd4Oqw7yQ75xOAYODybZQRzdeNucg7kS5wWKd2PNnMdYtJ2Vciy0QLylRmv42DGl5+AFL9izA6F1Rw==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2"
}
},
"Microsoft.NET.Test.Sdk": {
"type": "Direct",
"requested": "[17.11.1, )",
"resolved": "17.11.1",
"contentHash": "U3Ty4BaGoEu+T2bwSko9tWqWUOU16WzSFkq6U8zve75oRBMSLTBdMAZrVNNz1Tq12aCdDom9fcOcM9QZaFHqFg==",
"dependencies": {
"Microsoft.CodeCoverage": "17.11.1",
"Microsoft.TestPlatform.TestHost": "17.11.1"
}
},
"xunit": {
"type": "Direct",
"requested": "[2.9.2, )",
"resolved": "2.9.2",
"contentHash": "7LhFS2N9Z6Xgg8aE5lY95cneYivRMfRI8v+4PATa4S64D5Z/Plkg0qa8dTRHSiGRgVZ/CL2gEfJDE5AUhOX+2Q==",
"dependencies": {
"xunit.analyzers": "1.16.0",
"xunit.assert": "2.9.2",
"xunit.core": "[2.9.2]"
}
},
"xunit.runner.visualstudio": {
"type": "Direct",
"requested": "[2.8.2, )",
"resolved": "2.8.2",
"contentHash": "vm1tbfXhFmjFMUmS4M0J0ASXz3/U5XvXBa6DOQUL3fEz4Vt6YPhv+ESCarx6M6D+9kJkJYZKCNvJMas1+nVfmQ=="
},
"Microsoft.CodeCoverage": {
"type": "Transitive",
"resolved": "17.11.1",
"contentHash": "nPJqrcA5iX+Y0kqoT3a+pD/8lrW/V7ayqnEJQsTonSoPz59J8bmoQhcSN4G8+UJ64Hkuf0zuxnfuj2lkHOq4cA=="
},
"Microsoft.Extensions.Configuration": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "0J/9YNXTMWSZP2p2+nvl8p71zpSwokZXZuJW+VjdErkegAnFdO1XlqtA62SJtgVYHdKu3uPxJHcMR/r35HwFBA==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
"Microsoft.Extensions.Primitives": "8.0.0"
}
},
"Microsoft.Extensions.Configuration.Abstractions": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "3lE/iLSutpgX1CC0NOW70FJoGARRHbyKmG7dc0klnUZ9Dd9hS6N/POPWhKhMLCEuNN5nXEY5agmlFtH562vqhQ==",
"dependencies": {
"Microsoft.Extensions.Primitives": "8.0.0"
}
},
"Microsoft.Extensions.Diagnostics": {
"type": "Transitive",
"resolved": "8.0.1",
"contentHash": "doVPCUUCY7c6LhBsEfiy3W1bvS7Mi6LkfQMS8nlC22jZWNxBv8VO8bdfeyvpYFst6Kxqk7HBC6lytmEoBssvSQ==",
"dependencies": {
"Microsoft.Extensions.Configuration": "8.0.0",
"Microsoft.Extensions.Diagnostics.Abstractions": "8.0.1",
"Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0"
}
},
"Microsoft.Extensions.Diagnostics.Abstractions": {
"type": "Transitive",
"resolved": "8.0.1",
"contentHash": "elH2vmwNmsXuKmUeMQ4YW9ldXiF+gSGDgg1vORksob5POnpaI6caj1Hu8zaYbEuibhqCoWg0YRWDazBY3zjBfg==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2",
"Microsoft.Extensions.Options": "8.0.2"
}
},
"Microsoft.Extensions.Logging": {
"type": "Transitive",
"resolved": "8.0.1",
"contentHash": "4x+pzsQEbqxhNf1QYRr5TDkLP9UsLT3A6MdRKDDEgrW7h1ljiEPgTNhKYUhNCCAaVpQECVQ+onA91PTPnIp6Lw==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "8.0.1",
"Microsoft.Extensions.Logging.Abstractions": "8.0.2",
"Microsoft.Extensions.Options": "8.0.2"
}
},
"Microsoft.Extensions.Logging.Abstractions": {
"type": "Transitive",
"resolved": "8.0.2",
"contentHash": "nroMDjS7hNBPtkZqVBbSiQaQjWRDxITI8Y7XnDs97rqG3EbzVTNLZQf7bIeUJcaHOV8bca47s1Uxq94+2oGdxA==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2"
}
},
"Microsoft.Extensions.Primitives": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g=="
},
"Microsoft.TestPlatform.ObjectModel": {
"type": "Transitive",
"resolved": "17.11.1",
"contentHash": "E2jZqAU6JeWEVsyOEOrSW1o1bpHLgb25ypvKNB/moBXPVsFYBPd/Jwi7OrYahG50J83LfHzezYI+GaEkpAotiA==",
"dependencies": {
"System.Reflection.Metadata": "1.6.0"
}
},
"Microsoft.TestPlatform.TestHost": {
"type": "Transitive",
"resolved": "17.11.1",
"contentHash": "DnG+GOqJXO/CkoqlJWeDFTgPhqD/V6VqUIL3vINizCWZ3X+HshCtbbyDdSHQQEjrc2Sl/K3yaxX6s+5LFEdYuw==",
"dependencies": {
"Microsoft.TestPlatform.ObjectModel": "17.11.1",
"Newtonsoft.Json": "13.0.1"
}
},
"Newtonsoft.Json": {
"type": "Transitive",
"resolved": "13.0.1",
"contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A=="
},
"System.Reflection.Metadata": {
"type": "Transitive",
"resolved": "1.6.0",
"contentHash": "COC1aiAJjCoA5GBF+QKL2uLqEBew4JsCkQmoHKbN3TlOZKa2fKLz5CpiRQKDz0RsAOEGsVKqOD5bomsXq/4STQ=="
},
"xunit.abstractions": {
"type": "Transitive",
"resolved": "2.0.3",
"contentHash": "pot1I4YOxlWjIb5jmwvvQNbTrZ3lJQ+jUGkGjWE3hEFM0l5gOnBWS+H3qsex68s5cO52g+44vpGzhAt+42vwKg=="
},
"xunit.analyzers": {
"type": "Transitive",
"resolved": "1.16.0",
"contentHash": "hptYM7vGr46GUIgZt21YHO4rfuBAQS2eINbFo16CV/Dqq+24Tp+P5gDCACu1AbFfW4Sp/WRfDPSK8fmUUb8s0Q=="
},
"xunit.assert": {
"type": "Transitive",
"resolved": "2.9.2",
"contentHash": "QkNBAQG4pa66cholm28AxijBjrmki98/vsEh4Sx5iplzotvPgpiotcxqJQMRC8d7RV7nIT8ozh97957hDnZwsQ=="
},
"xunit.core": {
"type": "Transitive",
"resolved": "2.9.2",
"contentHash": "O6RrNSdmZ0xgEn5kT927PNwog5vxTtKrWMihhhrT0Sg9jQ7iBDciYOwzBgP2krBEk5/GBXI18R1lKvmnxGcb4w==",
"dependencies": {
"xunit.extensibility.core": "[2.9.2]",
"xunit.extensibility.execution": "[2.9.2]"
}
},
"xunit.extensibility.core": {
"type": "Transitive",
"resolved": "2.9.2",
"contentHash": "Ol+KlBJz1x8BrdnhN2DeOuLrr1I/cTwtHCggL9BvYqFuVd/TUSzxNT5O0NxCIXth30bsKxgMfdqLTcORtM52yQ==",
"dependencies": {
"xunit.abstractions": "2.0.3"
}
},
"xunit.extensibility.execution": {
"type": "Transitive",
"resolved": "2.9.2",
"contentHash": "rKMpq4GsIUIJibXuZoZ8lYp5EpROlnYaRpwu9Zr0sRZXE7JqJfEEbCsUriZqB+ByXCLFBJyjkTRULMdC+U566g==",
"dependencies": {
"xunit.extensibility.core": "[2.9.2]"
}
},
"binarylane.api": {
"type": "Project",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "[8.0.2, )",
"Microsoft.Extensions.Http": "[8.0.1, )",
"Microsoft.Extensions.Options": "[8.0.2, )"
}
},
"Microsoft.Extensions.Configuration.Binder": {
"type": "CentralTransitive",
"requested": "[8.0.2, )",
"resolved": "8.0.0",
"contentHash": "mBMoXLsr5s1y2zOHWmKsE9veDcx8h1x/c3rz4baEdQKTeDcmQAPNbB54Pi/lhFO3K431eEq6PFbMgLaa6PHFfA==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0"
}
},
"Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "CentralTransitive",
"requested": "[8.0.2, )",
"resolved": "8.0.2",
"contentHash": "3iE7UF7MQkCv1cxzCahz+Y/guQbTqieyxyaWKhrRO91itI9cOKO76OHeQDahqG4MmW5umr3CcCvGmK92lWNlbg=="
},
"Microsoft.Extensions.Http": {
"type": "CentralTransitive",
"requested": "[8.0.1, )",
"resolved": "8.0.1",
"contentHash": "kDYeKJUzh0qeg/AI+nSr3ffthmXYQTEb0nS9qRC7YhSbbuN4M4NPbaB77AJwtkTnCV9XZ7qYj3dkZaNcyl73EA==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2",
"Microsoft.Extensions.Diagnostics": "8.0.1",
"Microsoft.Extensions.Logging": "8.0.1",
"Microsoft.Extensions.Logging.Abstractions": "8.0.2",
"Microsoft.Extensions.Options": "8.0.2"
}
},
"Microsoft.Extensions.Options": {
"type": "CentralTransitive",
"requested": "[8.0.2, )",
"resolved": "8.0.2",
"contentHash": "dWGKvhFybsaZpGmzkGCbNNwBD1rVlWzrZKANLW/CcbFJpCEceMCGzT7zZwHOGBCbwM0SzBuceMj5HN1LKV1QqA==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
"Microsoft.Extensions.Primitives": "8.0.0"
}
},
"Microsoft.Extensions.Options.ConfigurationExtensions": {
"type": "CentralTransitive",
"requested": "[8.0.0, )",
"resolved": "8.0.0",
"contentHash": "0f4DMRqEd50zQh+UyJc+/HiBsZ3vhAQALgdkcQEalSH1L2isdC7Yj54M3cyo5e+BeO5fcBQ7Dxly8XiBBcvRgw==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
"Microsoft.Extensions.Configuration.Binder": "8.0.0",
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
"Microsoft.Extensions.Options": "8.0.0",
"Microsoft.Extensions.Primitives": "8.0.0"
}
}
}
}
}