Skip to content

Add agent-readable documentation shipped in the NuGet package - #404

Open
const-cloudinary wants to merge 1 commit into
masterfrom
feat/agent-readable-docs
Open

Add agent-readable documentation shipped in the NuGet package#404
const-cloudinary wants to merge 1 commit into
masterfrom
feat/agent-readable-docs

Conversation

@const-cloudinary

Copy link
Copy Markdown
Member

Agent-readable documentation for the .NET SDK

Ships version-matched task documentation inside the NuGet package, so an AI coding
agent working in a user's project reads docs that match the installed version instead of
relying on stale training data.

Follows the standard established in pycloudinary #450, cloudinary_npm #742/#744,
cloudinary_gem #596, and cloudinary_php #428, with the .NET-specific decisions and
deviations noted below.

Deliverables

Item Notes
docs/ — 15 pages Shipped in the .nupkg; see packaging decision below
examples/ — 8 runnable examples + runner Not packaged (rationale below)
AGENTS.md + CLAUDE.md Contributor guide for agents; CLAUDE.md is @AGENTS.md
SECURITY.md Private reporting via GitHub advisories — needs enabling on the repo
context7.json Real public_key, shared with the other Cloudinary SDKs
README.md restructured Same section list as npm, plus an error-handling section
LICENSE Already present and canonical MIT; unchanged

The only change to existing build configuration is one line in
CloudinaryDotNet.csproj.

Packaging decision (.NET / NuGet)

Nothing ships from a .nupkg unless told to, so docs are added with the same mechanism the
repo already uses for README.md and the icon, rather than a second mechanism:

<None Include="..\docs\**\*.md" Pack="true" PackagePath="docs\" />

Verified by building the package, unzipping it, and restoring it into a scratch project:

  • All 15 pages land at docs/ inside the .nupkg; no strays, no build output.
  • They are reachable on disk after restore at
    <global-packages>/cloudinarydotnet/<version>/docs/.
  • Multi-targeting does not affect this: docs sit next to lib/, not inside a TFM folder,
    so "is it in the package" and "is it reachable for the resolved assembly" are both yes
    for all three targets (netstandard1.3, netstandard2.0, net452).

Because the path contains the version, the README and docs banner give a locator rather
than a hardcoded path — run against a real restored package, not a checkout:

ROOT=$(dotnet nuget locals global-packages --list | awk '{print $2}')
ls "$ROOT"/cloudinarydotnet/*/docs

examples/ is deliberately not packaged (a deviation worth stating explicitly): they
are loose .cs files with a Main entry point, and a consumer's default compile glob
could pick them up and collide with their own entry point. examples/ is also outside the
library project's compile glob, so it is neither built nor analyzed by the main build —
confirmed by putting a deliberately StyleCop-violating file there and seeing the library
still build clean under TreatWarningsAsErrors. Every doc page carries its full runnable
flow inline, so the read path does not depend on the examples.

Verification

Every claim in these docs was produced by running code against live Claimable Clouds, not
by reading source. Snippet compilation used a harness that wraps each fragment; the harness
script is in the PR thread rather than committed (no linter/config added to the repo).

Check Result
Doc pages / examples / code blocks 15 / 8 / 76 blocks (65 C#, 9 bash, 1 JS, 1 JSON)
C# snippets compile 63/63 (0 errors). 2 excluded by design: a Razor @inject block and an illustrative constructor list
SDK symbols named in docs resolve 100% — implied by the above compiling against the real assembly
Internal links and anchors resolve 128/128
External URLs return 200 70/70 (placeholders and POST-only endpoints excluded)
Examples run live, in dependency order 8/8 on an accumulated cloud and 8/8 on a clean-slate cloud
Doc snippets executed live Yes — every documented behaviour below was executed
Error paths: no credentials / bad credentials 8/8 and 8/8 — friendly message, exit 1, zero stack traces
Built artifact Unpacked: 15/15 pages present, examples correctly absent, no strays
Existing test suite 613 passed / 0 failed on both base and branch (-f net8.0)
Full solution build Build succeeded, no new warnings

Note for reviewers on macOS/Linux: the test projects target net452;net8.0, so
dotnet test needs -f net8.0 and the .NET 8 runtime, or it fails looking for the
.NET Framework host. Recorded in AGENTS.md.

Behaviours found by execution that contradict a sibling SDK or a plausible guess

Each of these is now documented because it was measured, and several would have been wrong
had the npm/gem text been copied:

  1. API errors do not throw. They return in result.Error.Message + result.StatusCode.
    The assembly exports zero exception types, and Error has exactly one property —
    no error code. A try/catch around an upload catches nothing. This is the opposite of
    the npm/Python/Ruby model and is now the headline of docs/README.md and
    troubleshoot-errors.md. Reviewer feedback from npm #742 asked for "exceptions have
    structure — an error code and a message"; in this SDK that is not true, so the page
    documents StatusCode as the thing to branch on instead.
  2. URLs are HTTP by default (Api.Secure == false) — opposite of npm. Called out on
    every page that builds a URL.
  3. Moderation does not gate delivery. A pending asset returned HTTP 200, byte-identical
    in behaviour to a non-moderated control on the same cloud. This is the §7.1 trap and it
    reproduces here. Also: the status set has 6 values (including Overridden), and
    ModerationStatus is empty on the upload result while populated on GetResource.
  4. UploadLargeAsync with RawUploadParams stores a video as raw with HTTP 200 and no
    error — unusable forever. Chunk size < 5 MB throws (one of the few throwing paths).
  5. Overwrite = false on an existing public ID is a silent no-op returning the existing
    asset; the only signal is existing: true, available solely in JsonObj. The flag is
    absent when bytes were actually stored, so "flag present" means "nothing happened".
  6. folder: search matches nothing on a default (dynamic-folder) environment:
    folder:examples returned 0 while public_id:examples/* returned 4. A valid query
    silently returning zero.
  7. An unsubscribed add-on reports as HTTP 420 — the rate-limit code — not 401/403.
    Indistinguishable from real throttling by status alone.
  8. Undefined structured-metadata keys fail the entire upload, not just the metadata.
  9. Search index lags writes by a few seconds (measured: 0 results, then 1 at +5s) while
    GetResourceAsync is immediately consistent.
  10. MultipartFormDataContent cannot sign-upload from .NET without two fixes — .NET emits
    name=api_key unquoted, which Cloudinary's parser ignores, and StringContent adds a
    Content-Type. Both produce the misleading Upload preset must be specified when using unsigned upload. Verified both forms: unquoted → 400, quoted → 200. This bug was found
    by running the example, which is exactly the argument for doing so.
  11. CloudinaryConfiguration statics feed new Account(), not new Cloudinary()
    setting them and calling the parameterless constructor throws.
  12. Timeouts throw TaskCanceledException, bypassing the Error check; the message
    differs between the HttpClient default and an explicit Api.Timeout, so catch by type.
    Found when a second run of the example suite crashed with an unhandled exception.
  13. A Claimable Cloud's api_environment_variable includes the CLOUDINARY_URL= prefix
    passing it verbatim throws.
  14. Api.ApiProxy is a string and exists only on the netstandard2.0 target.
  15. Asset-ID coverage is partial: reads and related-resources accept an asset ID;
    update, rename, destroy, and URL building require the public ID. Documented as the
    honest "look up by asset ID, read PublicId, then act" pattern rather than claiming
    parity.

platform-capabilities.md was rebuilt from reflection rather than copied: this SDK
does implement AnalyzeAsync and VisualSearchAsync natively (unlike some siblings),
and it has no create-named-transformation method — both rows differ from npm's table.

Review themes from npm #742 applied

Organised platform-capabilities.md by use case in grouped categories (upload / deliver /
manage / analyze / administer) rather than one flat feature list; tags moved under upload;
added the CLI, Skills, MCP servers, and llms.txt indexes; account setup is a first-class
page (get-credentials.md) linked from the index; all Cloudinary doc links use the .md
form with the convention stated once in docs/README.md (the repo README links HTML first,
for people); linked the search-expression and transformation references instead of
paraphrasing; defined vague terms at point of use (signature validity = 1 hour, auto
explained); 423 Processing and the status page are in troubleshooting; size-limit guidance
distinguishes the 100 MB request ceiling from the product-environment maximum, says chunking
does not raise it, and shows reading real values from GetUsageAsync; add-on prerequisites
(registration + terms of service, no API) are stated; no hedging "A or B" advice; no
migrate-to-v2-style page.

Declined, with reasons: the reviewer's "exceptions have an error code and a message"
does not hold for this SDK (see #1) — documented StatusCode instead, and stated plainly
that Error.Message is the only field. Asset-ID preference is adopted but scoped to the
methods that actually accept one (#15). A third-party community integration is not
recommended anywhere, though platform-capabilities.md keeps the underlying fact that this
package has no tag helper, EF integration, or storage provider.

Needs a human

  1. Enable GitHub private vulnerability reporting on this repo — SECURITY.md links to
    /security/advisories/new, which 404s until a maintainer turns the feature on. This is
    the only item that leaves a broken link in shipped content.
  2. GitHub repo metadata — description, homepage, topics, social preview. Outward-facing;
    not changed unilaterally.
  3. Packaging change ack. AGENTS.md puts "changing the packaged file list" on the
    ask-first list, and this PR does exactly that (the one-line <None Include="..\docs\...">).
    Calling it out explicitly.

Resolved: the Context7 public_key is account-scoped, not per-library — confirmed
against cloudinary_npm, cloudinary_gem, pycloudinary, and cloudinary_php, which all
carry the same key with their own repo-specific url. Reused here, so no separate
submission is needed. That answers the question left open in the prior four PRs.

Notes / out of scope

  • CHANGELOG.md deliberately untouched — docs-only work, consistent with all four prior PRs.
  • No linter, formatter, or analyzer config added; no unrelated files reformatted.
  • samples/ left alone and labelled legacy in AGENTS.md.
  • Pre-existing, not addressed here: ~20 stale .nupkg files and legacy build folders
    (Cloudinary/, Core/, Shared/, Cloudinary.Test*/) are committed at the repo root.
    Mentioning it; cleaning it up belongs in its own PR.
  • .gitignore needed no change: bin//obj/ are already covered and the video example
    writes to the temp directory, not the repo.

AI coding agents working in a user's project have stale training data about
this SDK: they invent methods, use parameters from other versions, and assume
behaviour from sibling Cloudinary SDKs that does not hold here. Fix that by
shipping task documentation inside the package, so it is always version-matched
to the assembly the agent is looking at.

Follows the standard set by pycloudinary #450, cloudinary_npm #742/#744,
cloudinary_gem #596 and cloudinary_php #428.

Contents:

- docs/ - 14 task pages plus an index, shipped in the .nupkg. Covers install,
  configuration, upload, chunked video upload, signed browser uploads, image
  and video delivery, search and asset management, moderation, structured
  metadata, ASP.NET Core integration, and troubleshooting.
- examples/ - 8 runnable examples behind a single runner, deliberately NOT
  packaged: loose .cs files with a Main entry point could be picked up by a
  consumer's compile glob. Every doc page carries its flow inline instead.
- AGENTS.md (+ CLAUDE.md) - contributor guide for agents working on this repo.
- SECURITY.md, context7.json, restructured README.md.

Packaging uses the mechanism already in the csproj for README.md and the icon,
rather than introducing a second one. Verified by unpacking the .nupkg and by
restoring it into a scratch project: all 15 pages are present and reachable at
<global-packages>/cloudinarydotnet/<version>/docs/ for all three targets.

Every documented behaviour was established by executing it against live
Claimable Clouds, not by reading source. Notable findings that contradict a
sibling SDK or a plausible guess, and are now documented:

- Cloudinary API errors are returned in result.Error, never thrown. The
  assembly exports no exception types and Error carries only Message, so
  StatusCode is the only thing to branch on. try/catch around a call catches
  nothing.
- URLs are HTTP by default (Api.Secure is false), unlike the Node SDK.
- Moderation does not gate delivery: a pending asset returns HTTP 200,
  identically to a non-moderated control. The status set has six values, and
  ModerationStatus is empty on the upload result but populated on a read.
- UploadLargeAsync with RawUploadParams stores a video as raw with HTTP 200 and
  no error, leaving an asset no transformation can ever touch.
- Overwrite = false on an existing public ID is a silent no-op; the only signal
  is an existing flag available solely in JsonObj.
- folder: search expressions match nothing on a dynamic-folder environment.
- An unsubscribed add-on reports as HTTP 420, the rate-limit code.
- Undefined structured-metadata keys fail the whole upload.
- Timeouts throw TaskCanceledException, bypassing the Error check.
- Signing an upload from .NET needs MultipartFormDataContent field names
  pre-quoted, or Cloudinary treats the request as unsigned.

Verified: 63/63 C# snippets compile, 128/128 internal links and anchors
resolve, 70/70 external URLs return 200, 8/8 examples run green against both
an accumulated and a clean-slate cloud, 8/8 error paths exit non-zero with a
readable message and no stack trace, and the unit suite is unchanged at
613 passed / 0 failed.

The context7.json public key is account-scoped and shared with the other
Cloudinary SDKs, so it is reused here rather than newly issued.

No CHANGELOG entry (docs-only), no linter or formatter added, and samples/ is
left untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant