Add agent-readable documentation shipped in the NuGet package - #404
Open
const-cloudinary wants to merge 1 commit into
Open
Add agent-readable documentation shipped in the NuGet package#404const-cloudinary wants to merge 1 commit into
const-cloudinary wants to merge 1 commit into
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
docs/— 15 pages.nupkg; see packaging decision belowexamples/— 8 runnable examples + runnerAGENTS.md+CLAUDE.mdCLAUDE.mdis@AGENTS.mdSECURITY.mdcontext7.jsonpublic_key, shared with the other Cloudinary SDKsREADME.mdrestructuredLICENSEThe only change to existing build configuration is one line in
CloudinaryDotNet.csproj.Packaging decision (.NET / NuGet)
Nothing ships from a
.nupkgunless told to, so docs are added with the same mechanism therepo already uses for
README.mdand the icon, rather than a second mechanism:Verified by building the package, unzipping it, and restoring it into a scratch project:
docs/inside the.nupkg; no strays, no build output.<global-packages>/cloudinarydotnet/<version>/docs/.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:
examples/is deliberately not packaged (a deviation worth stating explicitly): theyare loose
.csfiles with aMainentry point, and a consumer's default compile globcould pick them up and collide with their own entry point.
examples/is also outside thelibrary 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 runnableflow 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).
@injectblock and an illustrative constructor list-f net8.0)Build succeeded, no new warningsNote for reviewers on macOS/Linux: the test projects target
net452;net8.0, sodotnet testneeds-f net8.0and 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:
result.Error.Message+result.StatusCode.The assembly exports zero exception types, and
Errorhas exactly one property —no error code. A
try/catcharound an upload catches nothing. This is the opposite ofthe npm/Python/Ruby model and is now the headline of
docs/README.mdandtroubleshoot-errors.md. Reviewer feedback from npm #742 asked for "exceptions havestructure — an error code and a message"; in this SDK that is not true, so the page
documents
StatusCodeas the thing to branch on instead.Api.Secure == false) — opposite of npm. Called out onevery page that builds a URL.
pendingasset returned HTTP 200, byte-identicalin 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), andModerationStatusis empty on the upload result while populated onGetResource.UploadLargeAsyncwithRawUploadParamsstores a video asrawwith HTTP 200 and noerror — unusable forever. Chunk size < 5 MB throws (one of the few throwing paths).
Overwrite = falseon an existing public ID is a silent no-op returning the existingasset; the only signal is
existing: true, available solely inJsonObj. The flag isabsent when bytes were actually stored, so "flag present" means "nothing happened".
folder:search matches nothing on a default (dynamic-folder) environment:folder:examplesreturned 0 whilepublic_id:examples/*returned 4. A valid querysilently returning zero.
Indistinguishable from real throttling by status alone.
GetResourceAsyncis immediately consistent.MultipartFormDataContentcannot sign-upload from .NET without two fixes — .NET emitsname=api_keyunquoted, which Cloudinary's parser ignores, andStringContentadds aContent-Type. Both produce the misleadingUpload preset must be specified when using unsigned upload. Verified both forms: unquoted → 400, quoted → 200. This bug was foundby running the example, which is exactly the argument for doing so.
CloudinaryConfigurationstatics feednew Account(), notnew Cloudinary()—setting them and calling the parameterless constructor throws.
TaskCanceledException, bypassing theErrorcheck; the messagediffers between the
HttpClientdefault and an explicitApi.Timeout, so catch by type.Found when a second run of the example suite crashed with an unhandled exception.
api_environment_variableincludes theCLOUDINARY_URL=prefix —passing it verbatim throws.
Api.ApiProxyis astringand exists only on thenetstandard2.0target.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 claimingparity.
platform-capabilities.mdwas rebuilt from reflection rather than copied: this SDKdoes implement
AnalyzeAsyncandVisualSearchAsyncnatively (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.mdby 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.txtindexes; account setup is a first-classpage (
get-credentials.md) linked from the index; all Cloudinary doc links use the.mdform 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,
autoexplained);
423 Processingand the status page are in troubleshooting; size-limit guidancedistinguishes 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
StatusCodeinstead, and stated plainlythat
Error.Messageis the only field. Asset-ID preference is adopted but scoped to themethods that actually accept one (#15). A third-party community integration is not
recommended anywhere, though
platform-capabilities.mdkeeps the underlying fact that thispackage has no tag helper, EF integration, or storage provider.
Needs a human
SECURITY.mdlinks to/security/advisories/new, which 404s until a maintainer turns the feature on. This isthe only item that leaves a broken link in shipped content.
not changed unilaterally.
AGENTS.mdputs "changing the packaged file list" on theask-first list, and this PR does exactly that (the one-line
<None Include="..\docs\...">).Calling it out explicitly.
Resolved: the Context7
public_keyis account-scoped, not per-library — confirmedagainst
cloudinary_npm,cloudinary_gem,pycloudinary, andcloudinary_php, which allcarry the same key with their own repo-specific
url. Reused here, so no separatesubmission is needed. That answers the question left open in the prior four PRs.
Notes / out of scope
CHANGELOG.mddeliberately untouched — docs-only work, consistent with all four prior PRs.samples/left alone and labelled legacy inAGENTS.md..nupkgfiles 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.
.gitignoreneeded no change:bin//obj/are already covered and the video examplewrites to the temp directory, not the repo.