Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space

[*.{cs,csx}]
indent_size = 4
# Use file-scoped namespaces throughout.
csharp_style_namespace_declarations = file_scoped:warning

# CA1711: the model type `RequestQueue` deliberately mirrors the Apify API resource name (and the
# JS/PHP/Java/Go/Rust sibling clients). Renaming it away from the reserved "Queue" suffix would break
# cross-client naming consistency, which the client requirements mandate. Justified suppression.
dotnet_diagnostic.CA1711.severity = none

[*.{csproj,props,targets}]
indent_size = 2

[*.{yml,yaml,json,md}]
indent_size = 2
77 changes: 77 additions & 0 deletions .github/workflows/dotnet-integration-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
name: .NET integration tests

# Language-specific workflow: only runs for the .NET client. Triggers on PRs to master that touch
# .NET client, test, project, example, or documentation code, and can be dispatched manually from any
# branch.
on:
pull_request:
branches: [master]
paths:
- '**/*.cs'
- '**/*.csproj'
- '**/*.sln'
- 'Directory.Build.props'
- '.editorconfig'
# The "Test examples" step validates the in-documentation snippets, so doc changes must re-run
# the workflow even though Markdown is not .NET code.
- 'docs/**'
- 'README.md'
- '.github/workflows/dotnet-integration-tests.yml'
workflow_dispatch:

# Avoid concurrent runs of the same ref racing on the shared test account.
concurrency:
group: dotnet-integration-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'

- name: Restore
run: dotnet restore

# Formatting gate mandated by the coding rules (dotnet format).
- name: Check formatting (dotnet format)
run: dotnet format --verify-no-changes

# Build with analyzers and warnings-as-errors (the static-analysis gate).
- name: Build (warnings as errors)
run: dotnet build --configuration Release --no-restore

# Offline unit tests (mock transport): prove the retry/error/signature logic without the API.
- name: Unit tests
run: dotnet test --configuration Release --no-build --filter Category=Unit

# Fail fast if the integration-test secret is missing or empty. Without this guard the
# integration tests silently skip (they self-skip when APIFY_TOKEN is unset), so a green run
# would not prove the API logic actually executed.
- name: Require APIFY_TOKEN secret
env:
APIFY_TOKEN: ${{ secrets.APIFY_TOKEN }}
run: |
if [ -z "${APIFY_TOKEN}" ]; then
echo "::error::APIFY_TOKEN secret is empty or missing; integration tests would not run against the API."
exit 1
fi

- name: Integration tests
env:
# The integration-test token is stored as a repository secret.
APIFY_TOKEN: ${{ secrets.APIFY_TOKEN }}
run: dotnet test --configuration Release --no-build --filter Category=Integration

# Standalone CI step that verifies the documentation examples work end-to-end against the live
# API (the Examples suite runs each example).
- name: Test examples
env:
APIFY_TOKEN: ${{ secrets.APIFY_TOKEN }}
run: dotnet test --configuration Release --no-build --filter Category=Examples
127 changes: 127 additions & 0 deletions .github/workflows/dotnet-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
name: Publish .NET client

# Language-specific publish workflow for the .NET client. Triggered manually only (workflow_dispatch)
# so a maintainer deliberately decides when a release is cut. The release version is the single source
# of truth in src/Apify.Client/Apify.Client.csproj (<Version>). This workflow packs the library,
# pushes it to NuGet.org, tags the release, and creates the GitHub release.
#
# Publishing uses a NuGet API key read from the NUGET_API_KEY repository secret. NuGet.org Trusted
# Publishing (OIDC) is not used because, per the official docs, it is still being rolled out gradually
# and is not generally available, so it is not an officially supported publishing mechanism we can rely
# on. If/when Trusted Publishing reaches GA, switch to the NuGet/login OIDC flow and drop this secret.
on:
workflow_dispatch:
inputs:
dry_run:
description: 'Run all checks and pack, but do not push, tag, or release.'
type: boolean
default: false

# Never allow two publish runs to race; publishing two releases concurrently is hard to undo.
concurrency:
group: dotnet-publish
cancel-in-progress: false

permissions:
contents: write # create the tagged GitHub release

jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

# A release must only ever be cut from master.
- name: Require master branch
run: |
if [ "${GITHUB_REF}" != "refs/heads/master" ]; then
echo "::error::Publishing is only allowed from master, but this run is on '${GITHUB_REF}'."
exit 1
fi

- name: Set up .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'

- name: Restore
run: dotnet restore

# Gate the release on the same quality bar as CI so a broken build can never be published.
- name: Check formatting (dotnet format)
run: dotnet format --verify-no-changes

- name: Build (warnings as errors)
run: dotnet build --configuration Release --no-restore

- name: Unit tests
run: dotnet test --configuration Release --no-build --filter Category=Unit

- name: Resolve version from csproj
id: version
run: |
version=$(grep -oPm1 '(?<=<Version>)[^<]+' src/Apify.Client/Apify.Client.csproj)
if ! echo "${version}" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "::error::<Version> '${version}' is not a bare semver (X.Y.Z)."
exit 1
fi
echo "tag=v${version}" >> "$GITHUB_OUTPUT"
echo "Resolved release tag: v${version}"

- name: Ensure tag does not already exist
env:
TAG: ${{ steps.version.outputs.tag }}
run: |
if git ls-remote --exit-code --tags origin "${TAG}" >/dev/null 2>&1; then
echo "::error::Tag ${TAG} already exists on origin; bump <Version> first."
exit 1
fi

- name: Pack
run: dotnet pack src/Apify.Client/Apify.Client.csproj --configuration Release --no-build --output ./artifacts

# Fail fast if the publish secret is missing so a run cannot silently pass without publishing.
- name: Require NUGET_API_KEY secret
if: ${{ github.event.inputs.dry_run != 'true' }}
env:
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
run: |
if [ -z "${NUGET_API_KEY}" ]; then
echo "::error::NUGET_API_KEY secret is empty or missing; cannot publish to NuGet.org."
exit 1
fi

- name: Push to NuGet
if: ${{ github.event.inputs.dry_run != 'true' }}
env:
# The NuGet.org API key is stored as a repository secret.
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
run: |
dotnet nuget push "./artifacts/*.nupkg" \
--api-key "${NUGET_API_KEY}" \
--source https://api.nuget.org/v3/index.json \
--skip-duplicate

- name: Create and push release tag
if: ${{ github.event.inputs.dry_run != 'true' }}
env:
TAG: ${{ steps.version.outputs.tag }}
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag -a "${TAG}" -m "Release ${TAG}"
git push origin "${TAG}"

- name: Create GitHub release
if: ${{ github.event.inputs.dry_run != 'true' }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ steps.version.outputs.tag }}
run: |
gh release create "${TAG}" \
--title "${TAG}" \
--notes "Apify .NET client ${TAG}. See CHANGELOG.md for details." \
./artifacts/*.nupkg
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,11 @@ CodeCoverage/
*.VisualState.xml
TestResult.xml
nunit-*.xml

# .NET build output
bin/
obj/

# IDE
.vs/
*.user
36 changes: 36 additions & 0 deletions Apify.Client.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{ED3360CB-7E9D-4B24-8CF1-E80489F99D61}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Apify.Client", "src\Apify.Client\Apify.Client.csproj", "{9A0401C4-477A-4111-ABC1-43D95430E810}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{624E02B9-A559-4043-AB5D-2A5CCFC19DA3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Apify.Client.Tests", "tests\Apify.Client.Tests\Apify.Client.Tests.csproj", "{6E8E3DF4-F6BA-48A6-8E07-18668FC9A7F6}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{9A0401C4-477A-4111-ABC1-43D95430E810}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9A0401C4-477A-4111-ABC1-43D95430E810}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9A0401C4-477A-4111-ABC1-43D95430E810}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9A0401C4-477A-4111-ABC1-43D95430E810}.Release|Any CPU.Build.0 = Release|Any CPU
{6E8E3DF4-F6BA-48A6-8E07-18668FC9A7F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6E8E3DF4-F6BA-48A6-8E07-18668FC9A7F6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6E8E3DF4-F6BA-48A6-8E07-18668FC9A7F6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6E8E3DF4-F6BA-48A6-8E07-18668FC9A7F6}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{9A0401C4-477A-4111-ABC1-43D95430E810} = {ED3360CB-7E9D-4B24-8CF1-E80489F99D61}
{6E8E3DF4-F6BA-48A6-8E07-18668FC9A7F6} = {624E02B9-A559-4043-AB5D-2A5CCFC19DA3}
EndGlobalSection
EndGlobal
46 changes: 46 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Changelog

## 0.1.0

- Initial .NET client for the Apify API (spec `v2-2026-07-02T131926Z`).
- Resource clients for Actors, Actor versions and environment variables, builds, runs, datasets,
key-value stores, request queues, tasks, schedules, webhooks, webhook dispatches, the Apify Store,
users, and logs.
- Async-first API (`Task`-returning, `CancellationToken`-aware) with convenience helpers consistent
with the JS reference client: `Actor().CallAsync()`/`StartAsync()`, `ValidateInputAsync()`,
`DefaultBuildAsync()`, `LastRun()`, run `AbortAsync`/`MetamorphAsync`/`RebootAsync`/`ResurrectAsync`/
`ChargeAsync`/`WaitForFinishAsync`, dataset `ListItemsAsync`/`DownloadItemsAsync`/`PushItemsAsync`/
public URLs, key-value store records and public URLs, request queue batch add with retries, and log
streaming.
- Auto-paging lazy iteration (`IAsyncEnumerable`) across all collection clients (`IterateAsync`) and
dataset items (`IterateItemsAsync`), plus request-queue and Store iteration, matching the reference
client's paginated iterators.
- Run log redirection: `RunClient.GetStreamedLog(toLog, fromStart)` returns a `StreamedLog` that forwards
a run's live log to a sink one message at a time, and `Actor`/`Task` `CallAsync` accept a `log` sink that
redirects the run's log for the duration of the wait.
- Last-run accessors forward their `status`/`origin` filters to the run's nested dataset, key-value store,
request queue, and log clients.
- Binary-safe storage payloads: `KeyValueStoreRecord.Value` and `DownloadItemsAsync` return `byte[]`
(raw bytes), and `SetRecordAsync` accepts `byte[]`, so binary records and exports (e.g. XLSX) are
not corrupted; `SetRecordJsonAsync` serializes to JSON bytes.
- `RequestQueueRequest.UserData` and `ActorEnvVar` `Name`/`Value`/`IsSecret` omit the field when set to
`null` (rather than writing a JSON `null`), honoring the documented null-omit contract.
- `BatchAddRequestsAsync` requires a non-empty `UniqueKey` per request, splits batches by both the
25-request count limit and the ~9 MiB payload-size limit, dispatches chunks with up to
`BatchAddRequestsOptions.MaxParallel` concurrent calls (results merged in input order), and retries
only the requests the API reports unprocessed in a successful response. Consistent with the reference
client, a failed batch call reports that chunk's not-yet-processed requests as unprocessed rather
than throwing.
- `PaginationList<T>.Count` is the number of items in the page (matching the indexer); the total across
all pages is exposed as `Total`.
- `Datasets().GetOrCreateAsync()` and `KeyValueStores().GetOrCreateAsync()` accept an optional schema.
- `RequestQueue(id, RequestQueueClientOptions)` accepts `ClientKey` and `TimeoutSecs`;
`PaginateRequestsAsync()` accepts `PaginateRequestsOptions` (`Limit`, `MaxPageLimit`,
`ExclusiveStartId`, `Cursor`, `Filter`).
- Replaceable HTTP transport (`IHttpTransport`) with a default `HttpClient`-based implementation;
automatic retries with exponential backoff and jitter, growing per-attempt timeouts, and
HMAC-SHA256 storage URL signing.
- Public `ApifyClientVersion.ClientVersion` and `ApifyClientVersion.ApiSpecVersion` constants.
- Integration test suite, documentation with runnable examples, a data-model property reference
(`docs/models.md`), and CI workflows for integration tests and publishing (manual NuGet.org publish
using an API key from a repository secret).
18 changes: 18 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project>
<!--
Shared build settings for every project in the repository. Static-analysis and
warnings-as-errors are enabled here so the whole solution is held to one quality bar
(the coding rules mandate compiler/analyzer gates enforced in CI).
-->
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<AnalysisLevel>latest</AnalysisLevel>
<AnalysisMode>Recommended</AnalysisMode>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
</Project>
Loading
Loading