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
8 changes: 4 additions & 4 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ 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
# CA1711: the `RequestQueue*` types deliberately mirror the Apify API resource name and the reference
# client. Renaming them away from the "Queue" suffix would break cross-client naming consistency, which
# the client requirements mandate. The rule stays on for every other suffix.
dotnet_code_quality.CA1711.allowed_suffixes = Queue

[*.{csproj,props,targets}]
indent_size = 2
Expand Down
7 changes: 5 additions & 2 deletions .github/workflows/dotnet-integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ on:
- '**/*.cs'
- '**/*.csproj'
- '**/*.sln'
# The embedded response schemas change the validation of every response, and the script generates them.
- 'src/Apify.Client/Internal/ResponseSchemas.json'
- 'tools/**'
- 'Directory.Build.props'
- '.editorconfig'
# The "Test examples" step validates the in-documentation snippets, so doc changes must re-run
Expand Down Expand Up @@ -69,8 +72,8 @@ jobs:
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).
# Standalone CI step that verifies the documentation examples: the Examples suite runs each example
# end-to-end against the live API and compiles every C# snippet in the README and docs/.
- name: Test examples
env:
APIFY_TOKEN: ${{ secrets.APIFY_TOKEN }}
Expand Down
46 changes: 46 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,51 @@
# Changelog

## 0.7.0

- Bumped `ApifyClientVersion.ApiSpecVersion` to the Apify OpenAPI spec `v2-2026-10-20T121847Z` and
the project version to `0.7.0`.
- Added `RequestId` to `RunChargeOptions`, sent as `requestId` in the charge request body; it identifies
the Server Actor request being charged (required for multi-tenant Server Actor runs).
- Documented that the rental pricing model (`FLAT_PRICE_PER_MONTH`) is no longer supported and is not an
accepted `StoreListOptions.PricingModel` filter value.
- Added `InvalidResponseBodyException` (an `ApifyClientException`), thrown when a successful response's
body is not valid JSON, instead of a raw `System.Text.Json.JsonException`. It is not retried and exposes
the status code, HTTP method, path and raw body. An error status with an unparsable body keeps throwing
the `ApifyApiException` for its status code.
- Added `ResponseValidationException` (an `ApifyClientException`), thrown when a successful response does
not match the schema the OpenAPI specification declares for its operation (missing `data` envelope,
missing required properties, wrong JSON types, `null` where it is not allowed, a value outside the
documented format, pattern or range), instead of silently returning `null` or an empty object. The
exception lists the problems in `Issues`. Unknown properties and enum values are accepted. The schemas
are embedded and generated by `tools/generate_response_schemas.py`. Matches the reference client's
`ResponseValidationError`.
- Added `ChunkSize` to `ListOptions`, `StorageListOptions`, `ActorListOptions`, `StoreListOptions`,
`DatasetListItemsOptions`, `IterateKeysOptions` and `PaginateRequestsOptions`: the page size iterators
request (the smaller of it and the remaining `Limit`), matching the reference client's `chunkSize`.
`MaxPageLimit` stays as the older name of the same option. A non-positive `ChunkSize` throws
`ArgumentOutOfRangeException`.
- Changed `DatasetClient.IterateItemsAsync`: `Limit` now caps the dataset rows read instead of the items
yielded (so with `Clean`, `SkipEmpty` or `Unwind` the item count differs), matching the reference
client. Paging advances by the larger of the `X-Apify-Pagination-Count` header and the items returned,
so a header lagging behind freshly pushed items no longer ends the iteration, and it no longer stops at
the total an earlier page reported.
- Changed `StoreCollectionClient.IterateAsync`: `Limit` is now a cap on the total number of Actors rather
than the page size, and, unlike the reference client, paging advances by the response's `count`, so the
Store dropping Actors the caller cannot run no longer makes it repeat or skip Actors.
- A `Limit` of `0` now means no limit in all offset/limit iterators, and a negative `Offset` or `Limit`
throws `ArgumentOutOfRangeException` instead of being silently adjusted.
- Nested accessors of a run (`Dataset()`, `KeyValueStore()`, `RequestQueue()`, `Log()`) and of a build
(`Log()`) now inherit the run's or build's `WithTimeout` override.
- Added a documentation test that compiles every C# snippet in the README and `docs/`.
- Added `RunClient.IterateDatasetItemsAsync` (with `RunIterateDatasetItemsOptions`), which iterates a run's
default dataset while the run is still producing items, matching the reference client's
`iterateDatasetItems()`.
- Fixed the webhook integration tests: the API now requires a webhook condition to reference an existing
Actor ID, so they use the ID of the public `apify/hello-world` Actor instead of a made-up run ID.
- Added integration tests for Actor and task webhook listing, request-queue request update/list and
dataset item iteration, and narrowed the CA1711 analyzer suppression to the `Queue` suffix.
- Removed the experimental/AI notice from the NuGet package description (it stays in the top-level README).

## 0.6.0

- Bumped `ApifyClientVersion.ApiSpecVersion` to the Apify OpenAPI spec `v2-2026-10-19T133704Z` and
Expand Down
27 changes: 23 additions & 4 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ the `using` directives for whichever ones a file references:
| `Apify.Client.Resources` | Every resource client the entry point returns — `ActorClient`, `RunClient`, `BuildClient`, `DatasetClient`, `KeyValueStoreClient`, `RequestQueueClient`, `TaskClient`, `ScheduleClient`, `LogClient`, `UserClient`, `ActorVersionClient`, `ActorEnvVarClient`, the `…CollectionClient` types (including `ActorVersionCollectionClient`, `ActorEnvVarCollectionClient`, `NestedWebhookCollectionClient`, and `WebhookDispatchCollectionClient`), etc. |
| `Apify.Client.Models` | Data models returned by the clients — `Actor`, `ActorRun`, `Build`, `Dataset`, `RequestQueueRequest`, `ActorEnvVar`, `PaginationList<T>`, and so on. |
| `Apify.Client.Options` | The option/request objects passed into methods — `ActorStartOptions`, `ActorBuildOptions`, `ActorListOptions`, `RunListOptions`, `RunResurrectOptions`, `RunChargeOptions`, `MetamorphOptions`, `LastRunOptions`, `DatasetListItemsOptions`, `DatasetDownloadOptions`, `DownloadItemsFormat`, `GetRecordOptions`, `SetRecordOptions`, `ListKeysOptions`, `IterateKeysOptions`, `ListRequestsOptions`, `PaginateRequestsOptions`, `BatchAddRequestsOptions`, `RequestQueueClientOptions`, `TaskStartOptions`, `ValidateInputOptions`, `LogOptions`, `ListOptions`, `StorageListOptions`, `StoreListOptions`. |
| `Apify.Client.Exceptions` | `ApifyClientException` (common base), `ApifyApiException` (and its per-status subclasses) and `ApifyTransportException`. |
| `Apify.Client.Exceptions` | `ApifyClientException` (common base), `ApifyApiException` (and its per-status subclasses), `ApifyTransportException`, `InvalidResponseBodyException` and `ResponseValidationException`. |
| `Apify.Client.Http` | The replaceable transport: `IHttpTransport` and the default `HttpClientTransport`. |
| `System.Text.Json.Nodes` (BCL) | Not an Apify namespace, but required whenever you name the JSON escape-hatch types the client returns/accepts — `JsonObject`/`JsonNode` (e.g. dataset items, `GetInputAsync`/`UpdateInputAsync`, `GetStatisticsAsync`, `GetOpenApiDefinitionAsync`, `MonthlyUsageAsync`/`LimitsAsync`, `RequestQueueRequest.UserData`). Add `using System.Text.Json.Nodes;`. |

Expand Down Expand Up @@ -205,11 +205,30 @@ catch (ApifyApiException e)
`ApifyTransportException` is thrown instead when the request never reaches the API (network failure,
timeout, or DNS error) after all retries are exhausted.

Both `ApifyApiException` and `ApifyTransportException` extend the common `ApifyClientException`, so
`catch (ApifyClientException)` catches every error this client raises for a failed call without naming
either concrete type:
`InvalidResponseBodyException` is thrown when a successful response's body is not valid JSON, and
`ResponseValidationException` when it is valid JSON but does not match the schema the API specification
declares for that operation (for example the `data` envelope or a required property is missing, or a
property has the wrong JSON type). Unknown properties and enum values are accepted, so new API fields do
not break the client. The response arrived in full, so neither is
retried. Both expose `StatusCode`, `HttpMethod`, `Path` and the raw `Body`. An error status with an
unparsable body throws the `ApifyApiException` for its status code, with the raw body in its message.

`ApifyApiException`, `ApifyTransportException`, `InvalidResponseBodyException` and
`ResponseValidationException` extend the common
`ApifyClientException`, so `catch (ApifyClientException)` catches every error this client raises for a
failed call without naming the concrete types:

```csharp
using System;
using Apify.Client;
using Apify.Client.Exceptions;

var client = new ApifyClient("my-api-token");
try
{
var user = await client.Me().GetAsync();
Console.WriteLine(user?.Username);
}
catch (ApifyClientException e)
{
Console.WriteLine("client call failed: " + e.Message);
Expand Down
1 change: 1 addition & 0 deletions docs/actors.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Access the Actor collection with `client.Actors()` and a specific Actor with `cl
| `Desc` | `bool?` | Sort newest-first when `true`. |
| `My` | `bool?` | Return only Actors owned by the current account when `true`. |
| `SortBy` | `string?` | Field to sort by (e.g. `createdAt`, `lastRunStartedAt`). |
| `ChunkSize` | `int?` | Page size used by `IterateAsync` (positive; the smaller of this and the remaining `Limit` is requested per call). Never sent to the API. |

```csharp
using System;
Expand Down
2 changes: 1 addition & 1 deletion docs/builds.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Access the account-wide build collection with `client.Builds()`, an Actor's buil

## Collection

- `ListAsync(ListOptions? options = null)` → `PaginationList<Build>` (`Offset`, `Limit`, `Desc`).
- `ListAsync(ListOptions? options = null)` → `PaginationList<Build>` (`Offset`, `Limit`, `Desc`; `ChunkSize` sizes the pages of `IterateAsync`).
- `IterateAsync(ListOptions? options = null)` → `IAsyncEnumerable<Build>` (lazy, all pages).

## Single build — `client.Build(buildId)`
Expand Down
7 changes: 4 additions & 3 deletions docs/misc.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,20 @@ Browse public Actors in the [Apify Store](https://apify.com/store).

- `ListAsync(StoreListOptions? options = null)` → `PaginationList<ActorStoreListItem>` (one page).
- `IterateAsync(StoreListOptions? options = null)` → `IAsyncEnumerable<ActorStoreListItem>` (lazy, all
pages; `Limit` is the page size).
pages; `Limit` caps the total number of Actors yielded and `ChunkSize` sets the page size).

`StoreListOptions` fields:

| Field | Type | Description |
|---|---|---|
| `Offset` | `int?` | Number of Actors to skip from the start. |
| `Limit` | `int?` | Maximum number of Actors to return in the page. |
| `Limit` | `int?` | Maximum number of Actors to return (for `IterateAsync`, in total across all pages). |
| `ChunkSize` | `int?` | Page size used by `IterateAsync` (positive; the smaller of this and the remaining `Limit` is requested per call). Never sent to the API. |
| `Search` | `string?` | Full-text search string to filter Actors by. |
| `SortBy` | `string?` | Field to sort by (e.g. `popularity`, `newest`). |
| `Category` | `string?` | Restrict results to a Store category. |
| `Username` | `string?` | Restrict results to a given owner's Actors. |
| `PricingModel` | `string?` | Filter by pricing model (`FREE`, `FLAT_PRICE_PER_MONTH`, `PRICE_PER_DATASET_ITEM`, …). |
| `PricingModel` | `string?` | Filter by pricing model (`FREE`, `PRICE_PER_DATASET_ITEM`, `PAY_PER_EVENT`; the rental model `FLAT_PRICE_PER_MONTH` is no longer supported). |
| `IncludeUnrunnableActors` | `bool?` | Include Actors that cannot currently be run. |
| `AllowsAgenticUsers` | `bool?` | Only Actors that permit agentic (automated) users. |
| `ResponseFormat` | `string?` | Requested response format. |
Expand Down
27 changes: 26 additions & 1 deletion docs/runs.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Access the account-wide run collection with `client.Runs()`, an Actor's or task'
- `IterateAsync(ListOptions? options = null, RunListOptions? filter = null)` → `IAsyncEnumerable<ActorRun>`
(lazy, all pages).

`ListOptions` fields: `Offset`, `Limit`, `Desc` (standard pagination). `RunListOptions` fields:
`ListOptions` fields: `Offset`, `Limit`, `Desc` (standard pagination; `Limit` `0` or unset means no cap) and `ChunkSize` (the page size `IterateAsync` requests; the smaller of it and the remaining `Limit` is used per call). `RunListOptions` fields:
`Status` (`IReadOnlyList<string>?`, filter by one or more run statuses such as `SUCCEEDED`/`RUNNING`),
`StartedAfter` and `StartedBefore` (ISO 8601 bounds, honoured only by the Actor- and task-scoped run
collections).
Expand All @@ -27,6 +27,10 @@ collections).
- `WaitForFinishAsync(int? waitSecs = null)` → `ActorRun` — client-side polling until terminal (`null`
waits indefinitely). A run id that keeps answering 404 for 3 seconds straight (replica lag resolves
faster than that) throws `NotFoundException` instead of polling for the rest of `waitSecs`.
- `IterateDatasetItemsAsync(RunIterateDatasetItemsOptions? options = null)` → `IAsyncEnumerable<JsonNode?>` —
lazily iterates the run's default dataset while the run is still producing items: it reads the rows
below the dataset's `itemCount`, waits for the run to finish between polls, and after the run finishes
reads any remaining rows. On a `LastRun()` client it sticks to the run its first request resolved to.
- `Dataset()`, `KeyValueStore()`, `RequestQueue()` — the run's default storages. Their own
`GetAsync()`/`DeleteAsync()` throw on a 404 instead of resolving `null`/no-op, since the run itself
(not just the storage) could be what's missing; a record/request looked up by its own key or id
Expand All @@ -45,6 +49,26 @@ collections).
`MemoryMbytes` (`int?`), `TimeoutSecs` (`int?`), `MaxItems` (`int?`), `MaxTotalChargeUsd` (`double?`),
and `RestartOnError` (`bool?`). See [Actors](actors.md) for each field's meaning.

`RunIterateDatasetItemsOptions` fields (all optional): `Offset` (rows to skip), `Limit` (maximum rows to
scan; `null` or `0` for no limit), `ChunkSize` (rows per API call, default 1000), `PollIntervalSecs`
(seconds to wait for the run to finish between polls, default 5), and the item filters `Clean`, `Fields`,
`Omit`, `Unwind`, `SkipEmpty` and `SkipHidden` (same meaning as in `DatasetListItemsOptions`).

```csharp
using System;
using Apify.Client;
using Apify.Client.Options;

var client = new ApifyClient("my-api-token");
var run = await client.Actor("apify/hello-world").StartAsync();

// Items are yielded as the run produces them, until the run has finished and its dataset is drained.
await foreach (var item in client.Run(run.Id!).IterateDatasetItemsAsync(new RunIterateDatasetItemsOptions { ChunkSize = 100 }))
{
Console.WriteLine(item);
}
```

`RunChargeOptions` describes a pay-per-event charge and is built via its constructor
`RunChargeOptions(string eventName, int? count = null, string? idempotencyKey = null)`:

Expand All @@ -53,6 +77,7 @@ and `RestartOnError` (`bool?`). See [Actors](actors.md) for each field's meaning
| `EventName` | `string` | Name of the pay-per-event event to charge for (required, non-empty). |
| `Count` | `int?` | Number of event occurrences to charge (defaults to 1 server-side). |
| `IdempotencyKey` | `string?` | Key that deduplicates retried charges; auto-generated when omitted. |
| `RequestId` | `string?` | ID of the Server Actor request being charged (the `X-Actor-Request-ID` header of the incoming request); required for multi-tenant Server Actor runs. Set via object initializer. |

```csharp
using Apify.Client;
Expand Down
13 changes: 8 additions & 5 deletions docs/storages.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ reached from a run (`client.Run(id).Dataset()`, `.KeyValueStore()`, `.RequestQue
| `Desc` | `bool?` | Sort newest-first when `true`. |
| `Unnamed` | `bool?` | Include unnamed storages when `true` (they are excluded by default). |
| `Ownership` | `string?` | Filter by ownership (e.g. only storages owned by the current account). |
| `ChunkSize` | `int?` | Page size used by `IterateAsync` (positive; the smaller of this and the remaining `Limit` is requested per call). Never sent to the API. |

## Datasets

Expand All @@ -39,7 +40,9 @@ reached from a run (`client.Run(id).Dataset()`, `.KeyValueStore()`, `.RequestQue
- `GetStatisticsAsync()` → `JsonObject` (throws if the dataset does not exist).
- `CreateItemsPublicUrlAsync(DatasetListItemsOptions? options = null, int? expiresInSecs = null, DownloadItemsFormat? format = null)` → `string` (a signed public URL; `format` defaults to `Json`).

`DatasetListItemsOptions` selects and reshapes items: `Offset`/`Limit` (pagination), `Desc` (reverse
`DatasetListItemsOptions` selects and reshapes items: `Offset`/`Limit` (pagination: `Limit` is the number of
dataset rows read, in total across pages when iterating; `0` or unset means no cap), `ChunkSize` (the page
size `IterateItemsAsync` requests, never sent to the API), `Desc` (reverse
order), `Fields`/`OutputFields`/`Omit` (choose columns), `Unwind`/`Flatten` (restructure nested
fields), `Clean`/`SkipEmpty`/`SkipHidden`/`SkipFailedPages` (drop unwanted rows), `Simplified`,
`View`, and `Signature` (for signed public URLs).
Expand Down Expand Up @@ -99,7 +102,7 @@ await csvStream.CopyToAsync(file);
- `GetAsync()` → `KeyValueStore?`; `UpdateAsync(object newFields)` → `KeyValueStore`; `DeleteAsync()`.
- `ListKeysAsync(ListKeysOptions? options = null)` → `KeyValueStoreKeysPage` (one page).
- `IterateKeysAsync(IterateKeysOptions? options = null)` → `IAsyncEnumerable<KeyValueStoreKey>` — lazily
iterate every key across pages, fetching each page (sized by `MaxPageLimit`) on demand.
iterate every key across pages, fetching each page (sized by `ChunkSize`) on demand.
- `RecordExistsAsync(string key)` → `bool`.
- `GetRecordAsync(string key, GetRecordOptions? options = null)` → `KeyValueStoreRecord?`.
`KeyValueStoreRecord.Value` is a `byte[]` of the record's raw bytes (so binary records survive intact);
Expand All @@ -114,8 +117,8 @@ await csvStream.CopyToAsync(file);
`ListKeysOptions` fields: `Limit` (page size), `ExclusiveStartKey` (start after this key),
`Prefix` (only keys with this prefix), `Collection` (a named record collection), and `Signature`
(for signed public URLs). `IterateKeysOptions` fields: `Limit` (total across all pages),
`MaxPageLimit` (page size; client-side only, never sent to the API — defaults to 1000),
`ExclusiveStartKey`, `Collection`, and `Prefix`. `GetRecordOptions` fields: `Attachment` (request a
`ChunkSize` (page size; client-side only, never sent to the API — defaults to 1000; `MaxPageLimit` is its
older name, and `ChunkSize` wins if both are set), `ExclusiveStartKey`, `Collection`, and `Prefix`. `GetRecordOptions` fields: `Attachment` (request a
download disposition) and `Signature`. `SetRecordOptions` fields: `TimeoutSecs`, `DoNotRetryTimeouts`,
and `ContentEncoding` (upload a value that is already compressed — see below).

Expand Down Expand Up @@ -210,7 +213,7 @@ leaves unprocessed), `MaxParallel` (how many chunks are sent concurrently), and
`MinDelayBetweenUnprocessedRequestsRetriesMillis` (backoff before retrying unprocessed requests).
`ListRequestsOptions`/`PaginateRequestsOptions` fields: `Limit`, `ExclusiveStartId`, `Cursor`, and
`Filter` (an `IReadOnlyList<string>?` — one or more of `"pending"`/`"locked"`, so several states can be
requested at once); `PaginateRequestsOptions` also has `MaxPageLimit`.
requested at once); `PaginateRequestsOptions` also has `ChunkSize` (page size, defaults to 1000; `MaxPageLimit` is its older name).

```csharp
using System;
Expand Down
Loading
Loading