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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## 0.2.0

- Bumped `ApifyClientVersion.ApiSpecVersion` to the Apify OpenAPI spec `v2-2026-08-05T133145Z` and the
project version to `0.2.0`.
- Added `TaskClient.PublishAsync()` and `TaskClient.UnpublishAsync()`, plus `ActorTask.IsPublic` and
`ActorTask.PublicConfig` (backed by the new `TaskPublicConfig` model), matching the reference
client's task publish/unpublish support.
- Removed the duplicated AI-disclaimer paragraph from `docs/README.md` and the `ApifyClient` XML
doc comment; it is now stated once, in the top-level `README.md`, per the client requirements.

## 0.1.4

- Bumped `ApifyClientVersion.ApiSpecVersion` to the Apify OpenAPI spec `v2-2026-07-13T092445Z` and the
Expand Down
4 changes: 1 addition & 3 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
# Apify .NET client documentation

> **Official, but experimental — AI-generated and AI-maintained.** This is an official Apify client,
> but it is experimental: it is generated and maintained by AI. Review the code before relying on it in
> production and report issues on the repository.
See the [top-level README](../README.md) for the client's official-but-experimental status.

A resource-oriented .NET client for the [Apify API](https://docs.apify.com/api/v2), mirroring the
official [JavaScript](https://github.com/apify/apify-client-js) reference client: start from an
Expand Down
14 changes: 14 additions & 0 deletions docs/models.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,20 @@ An entry returned when browsing the Apify Store.
| `Title` | `string?` | Human-readable title. |
| `CreatedAt` | `string?` | ISO 8601 creation timestamp. |
| `ModifiedAt` | `string?` | ISO 8601 last-modification timestamp. |
| `IsPublic` | `bool?` | Whether the task is published on its public landing page. Set via `TaskClient.PublishAsync()`/`UnpublishAsync()`, not directly. |
| `PublicConfig` | `TaskPublicConfig?` | The task's public landing page display configuration, or `null` if not set. |

## `TaskPublicConfig`

| Property | Type | Description |
|---|---|---|
| `PublishedAt` | `string?` | ISO 8601 timestamp the task was published, or `null` if unpublished. Read-only. |
| `SeoTitle` | `string?` | Name to display for search engines. |
| `SeoDescription` | `string?` | Description to display for search engines. |
| `Categorization` | `string?` | The task's category on its public landing page. |
| `InputSchemaFields` | `IReadOnlyList<string>?` | Input schema fields shown on the public landing page. |
| `DatasetName` | `string?` | Name of the dataset shown on the public landing page. |
| `DatasetView` | `string?` | View of the dataset shown on the public landing page. |

## `Dataset`

Expand Down
18 changes: 18 additions & 0 deletions docs/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ a specific task with `client.Task(id)`.
## Single task — `client.Task(id)`

- `GetAsync()` → `ActorTask?`; `UpdateAsync(object newFields)` → `ActorTask`; `DeleteAsync()`.
- `PublishAsync()` → `ActorTask` — publishes the task on its public landing page in Apify Store
(sets `isPublic: true`). The task's Actor must be public and the task must already have
`PublicConfig` set up. Requires write permission to both the task and its Actor. Publishing an
already published task does nothing.
- `UnpublishAsync()` → `ActorTask` — unpublishes the task (sets `isPublic: false`); `PublicConfig` is
preserved so the task can be published again without re-entering it. Requires write permission to
both the task and its Actor. Unpublishing a task that is not published does nothing.
- `StartAsync(object? input = null, TaskStartOptions? options = null)` → `ActorRun`.
- `CallAsync(object? input = null, TaskStartOptions? options = null, int? waitSecs = null, Action<string>? log = null)`
→ `ActorRun` (`log`, if set, redirects the run's live log to that sink for the duration of the wait).
Expand Down Expand Up @@ -42,3 +49,14 @@ await client.Task(task.Id!).UpdateInputAsync(new { message = "updated" });
var run = await client.Task(task.Id!).CallAsync(null, null, 120);
Console.WriteLine(run.Status);
```

Publishing a task requires its Actor to be public and the task to have `PublicConfig` set up first:

```csharp
var taskClient = client.Task(task.Id!);
await taskClient.UpdateAsync(new { publicConfig = new { seoTitle = "My task" } });
var published = await taskClient.PublishAsync();
Console.WriteLine(published.IsPublic == true);

await taskClient.UnpublishAsync();
```
2 changes: 1 addition & 1 deletion src/Apify.Client/Apify.Client.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<!-- NuGet package metadata (see the publish workflow). -->
<PackageId>Apify.Client</PackageId>
<Version>0.1.4</Version>
<Version>0.2.0</Version>
<Authors>Apify</Authors>
<Company>Apify</Company>
<Product>Apify API client for .NET</Product>
Expand Down
4 changes: 1 addition & 3 deletions src/Apify.Client/ApifyClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@ namespace Apify.Client;
/// </summary>
/// <remarks>
/// <para>
/// <b>Official, but experimental — AI-generated and AI-maintained.</b> This is an official Apify client,
/// but it is experimental: it is generated and maintained by AI. Review the code before relying on it in
/// production and report issues on the repository.
/// See the project's top-level README for the client's official-but-experimental status.
/// </para>
/// <para>
/// Construct it with an API token (and optional settings via <see cref="ApifyClientOptions"/>), then
Expand Down
4 changes: 2 additions & 2 deletions src/Apify.Client/ApifyClientVersion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ public static class ApifyClientVersion
/// The semantic version of this client library (see https://semver.org/). Changes to the public
/// interface other than additive ones are considered breaking changes.
/// </summary>
public const string ClientVersion = "0.1.4";
public const string ClientVersion = "0.2.0";

/// <summary>
/// The version of the Apify OpenAPI specification this client was generated and verified against.
/// Corresponds to the <c>info.version</c> field of the Apify OpenAPI document.
/// </summary>
public const string ApiSpecVersion = "v2-2026-07-13T092445Z";
public const string ApiSpecVersion = "v2-2026-08-05T133145Z";
}
10 changes: 10 additions & 0 deletions src/Apify.Client/Models/ActorTask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,14 @@ public ActorTask(JsonObject data)

/// <summary>When the task was last modified (ISO-8601 string).</summary>
public string? ModifiedAt => GetString("modifiedAt");

/// <summary>
/// Whether the task is published on its public landing page in Apify Store. Derived from
/// <see cref="PublicConfig"/>'s <c>publishedAt</c>; set it via <see cref="Resources.TaskClient.PublishAsync"/>
/// or <see cref="Resources.TaskClient.UnpublishAsync"/>, not by writing this field directly.
/// </summary>
public bool? IsPublic => GetBool("isPublic");

/// <summary>The task's public landing page display configuration, or <c>null</c> if not set.</summary>
public TaskPublicConfig? PublicConfig => Get("publicConfig") is JsonObject obj ? new TaskPublicConfig(obj) : null;
}
43 changes: 43 additions & 0 deletions src/Apify.Client/Models/TaskPublicConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System.Collections.Generic;
using System.Text.Json.Nodes;

namespace Apify.Client.Models;

/// <summary>
/// Public-facing display configuration for a task's public landing page in Apify Store.
/// </summary>
/// <remarks>
/// The task is published when <see cref="PublishedAt"/> is set and unpublished when it is
/// <c>null</c>. <see cref="PublishedAt"/> is read-only; use <see cref="Resources.TaskClient.PublishAsync"/>
/// and <see cref="Resources.TaskClient.UnpublishAsync"/> to change the publication state.
/// </remarks>
public sealed class TaskPublicConfig : ApifyResource
{
/// <summary>Wraps a raw public-config object.</summary>
/// <param name="data">The raw decoded object.</param>
public TaskPublicConfig(JsonObject data)
: base(data)
{
}

/// <summary>When the task was published (ISO-8601 string), or <c>null</c> if it is not published.</summary>
public string? PublishedAt => GetString("publishedAt");

/// <summary>Name to display for search engines such as Google.</summary>
public string? SeoTitle => GetString("seoTitle");

/// <summary>Description to display for search engines such as Google.</summary>
public string? SeoDescription => GetString("seoDescription");

/// <summary>The task's category on its public landing page.</summary>
public string? Categorization => GetString("categorization");

/// <summary>The input schema fields shown on the public landing page.</summary>
public IReadOnlyList<string>? InputSchemaFields => GetStringList("inputSchemaFields");

/// <summary>The name of the dataset shown on the public landing page.</summary>
public string? DatasetName => GetString("datasetName");

/// <summary>The view of the dataset shown on the public landing page.</summary>
public string? DatasetView => GetString("datasetView");
}
26 changes: 26 additions & 0 deletions src/Apify.Client/Resources/TaskClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,32 @@ public async Task<ActorTask> UpdateAsync(object newFields, CancellationToken can
/// <param name="cancellationToken">A token to cancel the request.</param>
public Task DeleteAsync(CancellationToken cancellationToken = default) => _ctx.DeleteResourceAsync("", cancellationToken);

/// <summary>
/// Publishes the task on its public landing page in Apify Store, by setting <c>isPublic</c>
/// through <see cref="UpdateAsync"/>.
/// </summary>
/// <remarks>
/// The task's Actor must be public and the task must already have its public display
/// configuration (<see cref="ActorTask.PublicConfig"/>) set up. Requires write permission to both
/// the task and its Actor. Publishing an already published task does nothing.
/// </remarks>
/// <param name="cancellationToken">A token to cancel the request.</param>
public Task<ActorTask> PublishAsync(CancellationToken cancellationToken = default) =>
UpdateAsync(new { isPublic = true }, cancellationToken);

/// <summary>
/// Unpublishes the task from its public landing page, by setting <c>isPublic</c> through
/// <see cref="UpdateAsync"/>.
/// </summary>
/// <remarks>
/// The public display configuration (<see cref="ActorTask.PublicConfig"/>) is preserved, so the
/// task can be published again without re-entering it. Requires write permission to both the task
/// and its Actor. Unpublishing a task that is not published does nothing.
/// </remarks>
/// <param name="cancellationToken">A token to cancel the request.</param>
public Task<ActorTask> UnpublishAsync(CancellationToken cancellationToken = default) =>
UpdateAsync(new { isPublic = false }, cancellationToken);

/// <summary>Starts the task and returns immediately with the created run.</summary>
/// <param name="input">Optionally overrides the task's stored input (<c>null</c> to use it).</param>
/// <param name="options">Optional run-start options.</param>
Expand Down
27 changes: 27 additions & 0 deletions tests/Apify.Client.Tests/Integration/TaskIntegrationTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Threading.Tasks;
using Apify.Client.Exceptions;
using Apify.Client.Options;
using Xunit;

Expand Down Expand Up @@ -61,4 +62,30 @@ public async Task TaskCrudFlow()
await client.Task(task.Id!).DeleteAsync();
}
}

[SkippableFact]
public async Task TaskPublishUnpublish()
{
var client = RequireClient();
var task = await client.Tasks().CreateAsync(TaskDef(UniqueName("task-publish")));
try
{
var tc = client.Task(task.Id!);

// Unpublishing an already-unpublished task is a documented no-op: it succeeds and returns
// the task unchanged (still not public).
var unpublished = await tc.UnpublishAsync();
Assert.Equal(task.Id, unpublished.Id);
Assert.True(unpublished.IsPublic != true);

// Publishing requires write permission to both the task and its Actor (apify/hello-world),
// which the test account does not have, so this is expected to fail rather than succeed.
var ex = await Assert.ThrowsAsync<ApifyApiException>(() => tc.PublishAsync());
Assert.True(ex.StatusCode is 400 or 403);
}
finally
{
await client.Task(task.Id!).DeleteAsync();
}
}
}
30 changes: 30 additions & 0 deletions tests/Apify.Client.Tests/Unit/RequestShapeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,36 @@ public async Task UpdateLimitsPutsToMeLimits()
Assert.Equal(100, JsonNode.Parse(request.Body)!["maxMonthlyUsageUsd"]!.GetValue<int>());
}

[Fact]
public async Task PublishPutsIsPublicTrue()
{
var transport = new MockTransport().QueueResponse(
200,
"{\"data\":{\"id\":\"t1\",\"isPublic\":true,\"publicConfig\":{\"publishedAt\":\"2026-01-01T00:00:00.000Z\",\"seoTitle\":\"My task\"}}}");
var task = await Client(transport).Task("t1").PublishAsync();

var request = transport.LastRequest;
Assert.Equal("PUT", request.Method);
Assert.Contains("/actor-tasks/t1", request.Uri, StringComparison.Ordinal);
Assert.True(JsonNode.Parse(request.Body)!["isPublic"]!.GetValue<bool>());
Assert.True(task.IsPublic);
Assert.Equal("2026-01-01T00:00:00.000Z", task.PublicConfig!.PublishedAt);
Assert.Equal("My task", task.PublicConfig!.SeoTitle);
}

[Fact]
public async Task UnpublishPutsIsPublicFalse()
{
var transport = new MockTransport().QueueResponse(200, "{\"data\":{\"id\":\"t1\",\"isPublic\":false}}");
var task = await Client(transport).Task("t1").UnpublishAsync();

var request = transport.LastRequest;
Assert.Equal("PUT", request.Method);
Assert.Contains("/actor-tasks/t1", request.Uri, StringComparison.Ordinal);
Assert.False(JsonNode.Parse(request.Body)!["isPublic"]!.GetValue<bool>());
Assert.False(task.IsPublic);
}

[Fact]
public async Task DatasetListItemsJoinsMultiValueParamsAsCsv()
{
Expand Down
Loading