From 901e5cad13f273bdf065459dbb51b80255cfbffb Mon Sep 17 00:00:00 2001 From: raj pandey Date: Wed, 8 Apr 2026 10:54:41 +0530 Subject: [PATCH 1/2] update: added skill files --- .cursor/rules/README.md | 5 + AGENTS.md | 49 +++ README.md | 2 + skills/README.md | 21 ++ skills/code-review/SKILL.md | 26 ++ .../references/code-review-checklist.md | 186 +++++++++++ skills/code-review/references/red-flags.md | 15 + skills/dev-workflow/SKILL.md | 28 ++ .../dev-workflow/references/repo-tooling.md | 67 ++++ skills/error-handling/SKILL.md | 27 ++ .../references/error-patterns.md | 274 ++++++++++++++++ skills/models-and-serialization/SKILL.md | 23 ++ .../references/serialization-patterns.md | 228 +++++++++++++ skills/query-building/SKILL.md | 35 ++ .../references/query-patterns.md | 280 ++++++++++++++++ skills/sdk-core-patterns/SKILL.md | 54 +++ .../references/aspnetcore-integration.md | 48 +++ .../references/sdk-architecture.md | 143 ++++++++ skills/testing/SKILL.md | 28 ++ skills/testing/references/testing-patterns.md | 309 ++++++++++++++++++ 20 files changed, 1848 insertions(+) create mode 100644 .cursor/rules/README.md create mode 100644 AGENTS.md create mode 100644 skills/README.md create mode 100644 skills/code-review/SKILL.md create mode 100644 skills/code-review/references/code-review-checklist.md create mode 100644 skills/code-review/references/red-flags.md create mode 100644 skills/dev-workflow/SKILL.md create mode 100644 skills/dev-workflow/references/repo-tooling.md create mode 100644 skills/error-handling/SKILL.md create mode 100644 skills/error-handling/references/error-patterns.md create mode 100644 skills/models-and-serialization/SKILL.md create mode 100644 skills/models-and-serialization/references/serialization-patterns.md create mode 100644 skills/query-building/SKILL.md create mode 100644 skills/query-building/references/query-patterns.md create mode 100644 skills/sdk-core-patterns/SKILL.md create mode 100644 skills/sdk-core-patterns/references/aspnetcore-integration.md create mode 100644 skills/sdk-core-patterns/references/sdk-architecture.md create mode 100644 skills/testing/SKILL.md create mode 100644 skills/testing/references/testing-patterns.md diff --git a/.cursor/rules/README.md b/.cursor/rules/README.md new file mode 100644 index 00000000..f5c1f870 --- /dev/null +++ b/.cursor/rules/README.md @@ -0,0 +1,5 @@ +# Cursor (optional) + +**Cursor** users: start at **[AGENTS.md](../../AGENTS.md)**. All conventions live in **`skills/*/SKILL.md`**. + +This folder only points contributors to **`AGENTS.md`** so editor-specific config does not duplicate the canonical docs. diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..df8ef7bc --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,49 @@ +# Contentstack .NET SDK – Agent guide + +**Universal entry point** for contributors and AI agents. Each skill has a short **`skills/*/SKILL.md`** entry; exhaustive patterns, checklists, and examples live in **`skills/*/references/*.md`** where present. + +## What this repo is + +| Field | Detail | +|-------|--------| +| **Name:** | [contentstack-dotnet](https://github.com/contentstack/contentstack-dotnet) | +| **Purpose:** | .NET SDK for Contentstack’s Content Delivery API (CDA)—fetch entries, assets, and run queries from .NET apps. | +| **Out of scope (if any):** | Do not bypass the SDK HTTP layer with ad-hoc `HttpClient` usage; all requests go through `HttpRequestHandler` (see `skills/sdk-core-patterns/SKILL.md`). | + +## Tech stack (at a glance) + +| Area | Details | +|------|---------| +| Language | C#; multi-targeting includes `netstandard2.0`, `net47`, `net472` (see project files under `Contentstack.Core/`). | +| Build | .NET SDK — solution [`Contentstack.Net.sln`](Contentstack.Net.sln); packages [`Contentstack.Core/`](Contentstack.Core/) (Delivery SDK), [`Contentstack.AspNetCore/`](Contentstack.AspNetCore/) (DI extensions). | +| Tests | xUnit; unit tests in [`Contentstack.Core.Unit.Tests/`](Contentstack.Core.Unit.Tests/) (no credentials); integration tests in [`Contentstack.Core.Tests/`](Contentstack.Core.Tests/) (requires `app.config` / API credentials). | +| Lint / coverage | No dedicated repo-wide lint/format CLI in CI. Security/static analysis: [CodeQL workflow](.github/workflows/codeql-analysis.yml). | +| Other | JSON: Newtonsoft.Json; package version: single source in [`Directory.Build.props`](Directory.Build.props). | + +## Commands (quick reference) + +| Command type | Command | +|--------------|---------| +| Build | `dotnet build Contentstack.Net.sln` | +| Test (unit) | `dotnet test Contentstack.Core.Unit.Tests/Contentstack.Core.Unit.Tests.csproj` | +| Test (integration) | `dotnet test Contentstack.Core.Tests/Contentstack.Core.Tests.csproj` (configure credentials locally) | + +CI: [`.github/workflows/unit-test.yml`](.github/workflows/unit-test.yml) restores, builds, and runs unit tests on Windows (.NET 7). Other workflows include [NuGet publish](.github/workflows/nuget-publish.yml), [branch checks](.github/workflows/check-branch.yml), [CodeQL](.github/workflows/codeql-analysis.yml), policy/SCA scans. + +## Where the documentation lives: skills + +| Skill | Path | What it covers | +|-------|------|----------------| +| Dev workflow | [`skills/dev-workflow/SKILL.md`](skills/dev-workflow/SKILL.md) | Solution layout, build/test commands, versioning, CI entry points. | +| SDK core patterns | [`skills/sdk-core-patterns/SKILL.md`](skills/sdk-core-patterns/SKILL.md) | Architecture, `ContentstackClient`, HTTP layer, DI, plugins. | +| Query building | [`skills/query-building/SKILL.md`](skills/query-building/SKILL.md) | Fluent query API, operators, pagination, sync, taxonomy. | +| Models and serialization | [`skills/models-and-serialization/SKILL.md`](skills/models-and-serialization/SKILL.md) | Entry/Asset models, JSON converters, collections. | +| Error handling | [`skills/error-handling/SKILL.md`](skills/error-handling/SKILL.md) | Exception hierarchy, `ErrorMessages`, API error parsing. | +| Testing | [`skills/testing/SKILL.md`](skills/testing/SKILL.md) | Unit vs integration tests, AutoFixture, `IntegrationTestBase`. | +| Code review | [`skills/code-review/SKILL.md`](skills/code-review/SKILL.md) | PR checklist for this SDK. | + +An index with “when to use” hints is in [`skills/README.md`](skills/README.md). + +## Using Cursor (optional) + +If you use **Cursor**, [`.cursor/rules/README.md`](.cursor/rules/README.md) only points to **`AGENTS.md`**—the same conventions as for everyone else. Canonical guidance remains in **`skills/*/SKILL.md`** and **`skills/*/references/`**. diff --git a/README.md b/README.md index ea063835..a07ff77c 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ .NET SDK for Contentstack's Content Delivery API +Contributor and agent conventions: see **[AGENTS.md](AGENTS.md)**. + ## Getting Started This guide will help you get started with our .NET SDK to build apps powered by Contentstack. diff --git a/skills/README.md b/skills/README.md new file mode 100644 index 00000000..aafc465d --- /dev/null +++ b/skills/README.md @@ -0,0 +1,21 @@ +# Skills – Contentstack .NET SDK + +Source of truth for detailed guidance. Read **[AGENTS.md](../AGENTS.md)** first, then open the skill that matches your task. + +## When to use which skill + +| Skill folder | Use when | +|--------------|----------| +| `dev-workflow` | Building the solution, versioning, CI workflows, onboarding, PR prep. | +| `sdk-core-patterns` | Architecture, `ContentstackClient`, HTTP layer, DI, plugins, request flow. | +| `query-building` | Query operators, fluent API, pagination, sync, taxonomy, `Query.cs`. | +| `models-and-serialization` | Entry/Asset models, JSON converters, `ContentstackCollection`, serialization. | +| `error-handling` | Exceptions, `ErrorMessages`, parsing API errors. | +| `testing` | Writing or debugging unit/integration tests, coverage, test layout. | +| `code-review` | Reviewing a PR against SDK-specific checklist. | + +Each folder contains **`SKILL.md`** with YAML frontmatter (`name`, `description`) for agent discovery. Long-form content is under **`references/`** when applicable—open `SKILL.md` first, then follow links. + +### Cursor + +In Cursor, you can also reference a skill in chat with `@skills/` (for example `@skills/testing`). diff --git a/skills/code-review/SKILL.md b/skills/code-review/SKILL.md new file mode 100644 index 00000000..f62068c0 --- /dev/null +++ b/skills/code-review/SKILL.md @@ -0,0 +1,26 @@ +--- +name: code-review +description: SDK-specific PR review checklist for the Contentstack .NET SDK — covers breaking changes, HTTP layer correctness, exception handling, serialization, fluent API patterns, configuration, test coverage, multi-targeting, plugin lifecycle, and internal visibility. Use when reviewing pull requests, examining code changes, or performing code quality assessments on this SDK. +--- + +# Code Review + +## When to use + +- Reviewing a PR or diff against SDK conventions. +- Self-review before opening a PR. + +## Severity levels + +- **Critical** — Must fix before merge (correctness, breaking changes, security) +- **Important** — Should fix (maintainability, SDK patterns, consistency) +- **Suggestion** — Consider improving (style, optimization) + +Category-by-category checklists (breaking changes, HTTP, exceptions, serialization, fluent API, config, tests, multi-targeting, plugins, visibility) live in the reference docs below—not duplicated here. + +## Reference + +| Document | Contents | +|----------|----------| +| [references/code-review-checklist.md](references/code-review-checklist.md) | Full markdown checklists per area | +| [references/red-flags.md](references/red-flags.md) | Short anti-pattern list for quick scan | diff --git a/skills/code-review/references/code-review-checklist.md b/skills/code-review/references/code-review-checklist.md new file mode 100644 index 00000000..2e74306f --- /dev/null +++ b/skills/code-review/references/code-review-checklist.md @@ -0,0 +1,186 @@ +# Code Review Checklist + +## Breaking Changes Checklist + +```markdown +## Breaking Changes Review +- [ ] No public method signatures removed or changed without [Obsolete] deprecation +- [ ] No [JsonProperty] values changed (would break consumer deserialization silently) +- [ ] No ContentstackOptions public property removed +- [ ] New required options have defaults (don't break existing consumers who don't set them) +- [ ] No namespace renames without backward-compatible type aliases +- [ ] No IContentstackPlugin interface signature changed +- [ ] Version bump planned if breaking change is intentional (Directory.Build.props) +``` + +## HTTP Layer Checklist + +```markdown +## HTTP Layer Review +- [ ] All HTTP calls route through HttpRequestHandler.ProcessRequest +- [ ] No HttpClient instantiation anywhere in the PR +- [ ] New query params added to UrlQueries dict (not directly to URL string) +- [ ] New field-level filters added to QueryValueJson dict +- [ ] New headers added via Headers parameter to ProcessRequest +- [ ] Branch header uses "branch" key, passed as separate Branch parameter +- [ ] No hardcoded URLs — BaseUrl comes from Config.BaseUrl +- [ ] Live preview URL resolved via Config.getBaseUrl() — not hardcoded +- [ ] ProcessRequest result (string JSON) parsed, not further HTTP calls made +``` + +## Exception Handling Checklist + +```markdown +## Exception Handling Review +- [ ] Domain-specific exception type used (QueryFilterException, AssetException, etc.) +- [ ] No bare `throw new Exception(...)` or `throw new ContentstackException(...)` +- [ ] All message strings sourced from ErrorMessages.cs constants +- [ ] No string literals in throw statements +- [ ] GetContentstackError(ex) called when catching WebException from HTTP calls +- [ ] ErrorCode, StatusCode, Errors preserved when re-wrapping exceptions +- [ ] New domain area has new exception class with factory methods +- [ ] New error messages added to correct section in ErrorMessages.cs +- [ ] FormatExceptionDetails(innerEx) used in ProcessingError factory methods +``` + +## Serialization Checklist + +```markdown +## Serialization Review +- [ ] All public properties mapping CDA JSON fields have [JsonProperty("snake_case")] +- [ ] No reliance on default Newtonsoft.Json camelCase or PascalCase matching +- [ ] Custom deserialization uses [CSJsonConverter] + JsonConverter subclass +- [ ] JsonConverter placed in Contentstack.Core/Internals/ (internal class) +- [ ] No System.Text.Json usage +- [ ] No JsonConvert.DeserializeObject with hardcoded type outside of converter +- [ ] ContentstackCollection used for list responses (not List directly) +- [ ] "entries" token used for entry collection, "assets" for asset collection +``` + +## Fluent API Checklist + +```markdown +## Fluent API Review +- [ ] Every Query filter/operator method returns `return this;` +- [ ] Null key validated at start of method → QueryFilterException.Create() +- [ ] Empty string key validated → QueryFilterException.Create() +- [ ] Operator value stored in QueryValueJson[key][$operator] nested dict +- [ ] URL-level params stored in UrlQueries[key] +- [ ] Method name follows verb+noun pattern (GreaterThan, ContainedIn, NotExists) +- [ ] No mutation of QueryValueJson or UrlQueries outside of the Query class itself +- [ ] And()/Or() accept Query[] (not raw dictionaries) +``` + +## Configuration Checklist + +```markdown +## Configuration Review +- [ ] New options added to ContentstackOptions (public class), not Config (internal) +- [ ] New property has XML doc comment +- [ ] Default value set in ContentstackOptions() constructor or property initializer +- [ ] ContentstackClient constructor reads new option and passes to Config +- [ ] Config never exposed as public property +- [ ] New option tested in ContentstackOptionsUnitTests.cs +- [ ] ASP.NET Core binding works (IOptions path verified) +``` + +## Test Coverage Checklist + +```markdown +## Test Coverage Review +- [ ] Unit test for each new public Query method (QueryValueJson assertion via reflection) +- [ ] Unit test for null key input → QueryFilterException +- [ ] Unit test for empty key input → QueryFilterException +- [ ] Unit test for fluent return (Assert.Equal(query, result)) +- [ ] Integration test file in Integration/{FeatureName}Tests/ subfolder +- [ ] Integration test class extends IntegrationTestBase +- [ ] Integration test constructor takes ITestOutputHelper output +- [ ] CreateClient() used (not manual ContentstackClient construction) +- [ ] LogArrange/LogAct/LogAssert used in correct order +- [ ] TestAssert.* used (not raw Assert.*) +- [ ] [Fact(DisplayName = "FeatureArea - Component Description")] present +- [ ] Happy path test (valid params → expected response) +- [ ] Error path test (invalid params or not found → expected exception) +``` + +## Multi-Targeting Checklist + +```markdown +## Multi-Targeting Review +- [ ] No HttpClient (netstandard2.0 HttpClient has behavioural differences from net4x) +- [ ] No System.Text.Json (not available without separate package in netstandard2.0) +- [ ] No record types (C# 9, requires LangVersion setting for net47/net472) +- [ ] No default interface implementations (C# 8, may affect net47) +- [ ] No nullable reference types without #nullable enable guard +- [ ] No top-level statements (not applicable to library projects but worth checking) +- [ ] Tested compile against netstandard2.0 target (or verified via CI) +``` + +## Plugin Lifecycle Checklist + +```markdown +## Plugin Lifecycle Review +- [ ] New feature that makes HTTP calls uses HttpRequestHandler (plugins run automatically) +- [ ] No WebRequest.Create() called directly in new model classes +- [ ] IContentstackPlugin interface not modified (breaking for all plugin consumers) +- [ ] RequestLoggingPlugin still works with any new request/response changes +- [ ] Plugin.OnRequest receives HttpWebRequest before send +- [ ] Plugin.OnResponse receives response string (can mutate/inspect) +``` + +## Internal Visibility Checklist + +```markdown +## Internal Visibility Review +- [ ] New utility/helper classes in Internals/ are marked `internal` +- [ ] New model types intended for consumers are in Models/ and `public` +- [ ] New configuration types are in Configuration/ and `public` +- [ ] No public exposure of Config, HttpRequestHandler, or VersionUtility +- [ ] InternalsVisibleTo not modified (already covers both test projects) +- [ ] New internal methods accessible in unit tests without changes +``` + +## Common Issues Found in Past PRs + +### Silent Deserialization Failures +`[JsonProperty]` omitted → field is always null at runtime, no exception. Verify all properties that map CDA JSON fields. + +### Exception Message in Throw +```csharp +// Bad +throw new QueryFilterException("Please provide valid params."); + +// Good +throw QueryFilterException.Create(innerEx); +// or +throw new QueryFilterException(ErrorMessages.QueryFilterError); +``` + +### Hardcoded Environment +```csharp +// Bad — breaks for consumers with different environments +mainJson["environment"] = "production"; + +// Correct — already done in Exec() +mainJson["environment"] = ContentTypeInstance.StackInstance.Config.Environment; +``` + +### Returning void from Query Method +```csharp +// Bad — breaks fluent chaining +public void SetMyParam(string value) { UrlQueries["my_param"] = value; } + +// Good +public Query SetMyParam(string value) { UrlQueries["my_param"] = value; return this; } +``` + +### Dictionary Not Initialized for QueryValueJson Entry +```csharp +// Bad — throws KeyNotFoundException or InvalidCastException +((Dictionary)QueryValueJson[key])["$op"] = value; + +// Good — guard with ContainsKey +if (!QueryValueJson.ContainsKey(key)) + QueryValueJson[key] = new Dictionary(); +((Dictionary)QueryValueJson[key])["$op"] = value; +``` diff --git a/skills/code-review/references/red-flags.md b/skills/code-review/references/red-flags.md new file mode 100644 index 00000000..3e5078d9 --- /dev/null +++ b/skills/code-review/references/red-flags.md @@ -0,0 +1,15 @@ +# SDK-specific red flags + +Quick scan for anti-patterns in PRs: + +``` +❌ new HttpClient() — use HttpRequestHandler +❌ throw new Exception("message") — use typed ContentstackException subclass +❌ "hardcoded_field_name" — use [JsonProperty] or ErrorMessages constant +❌ public Config GetConfig() — Config is internal by design +❌ return void — Query methods return Query (fluent) +❌ [JsonProperty] omitted — CDA uses snake_case; PascalCase won't deserialize +❌ in .csproj — use Directory.Build.props +``` + +For full category-by-category review, use [code-review-checklist.md](code-review-checklist.md). diff --git a/skills/dev-workflow/SKILL.md b/skills/dev-workflow/SKILL.md new file mode 100644 index 00000000..e640c9ca --- /dev/null +++ b/skills/dev-workflow/SKILL.md @@ -0,0 +1,28 @@ +--- +name: dev-workflow +description: Local development and CI workflow for the Contentstack .NET SDK — solution layout, dotnet build/test commands, package versioning in Directory.Build.props, GitHub Actions (unit tests, CodeQL, NuGet), and integration test credentials. Use when onboarding, running builds, preparing a PR, or finding where CI is defined. +--- + +# Dev workflow – Contentstack .NET SDK + +## When to use + +- Onboarding, building, or running tests locally. +- Finding which workflow applies to PRs, releases, or security scans. +- Maintainer tasks: DocFX, NuGet publish expectations. + +## Invariants + +- Version in **`Directory.Build.props`** only for package version. +- Integration tests need **`app.config`** (or equivalent) — do not commit secrets. + +## Reference + +| Document | Contents | +|----------|----------| +| [references/repo-tooling.md](references/repo-tooling.md) | Commands, CI, branch policy, CodeQL, NuGet release workflow, DocFX, SCA/policy scans, project layout | + +## Related skills + +- [SDK core patterns](../sdk-core-patterns/SKILL.md) — architecture and HTTP. +- [Testing](../testing/SKILL.md) — unit vs integration patterns and credentials. diff --git a/skills/dev-workflow/references/repo-tooling.md b/skills/dev-workflow/references/repo-tooling.md new file mode 100644 index 00000000..428a23fb --- /dev/null +++ b/skills/dev-workflow/references/repo-tooling.md @@ -0,0 +1,67 @@ +# Repo tooling — CI, release, docs, security + +## Local commands + +| Action | Command | +|--------|---------| +| Build | `dotnet build Contentstack.Net.sln` | +| Unit tests | `dotnet test Contentstack.Core.Unit.Tests/Contentstack.Core.Unit.Tests.csproj` | +| Integration tests | `dotnet test Contentstack.Core.Tests/Contentstack.Core.Tests.csproj` (credentials) | + +Package version: [`Directory.Build.props`](../../../Directory.Build.props) (``). + +## CI — unit tests + +[`.github/workflows/unit-test.yml`](../../../.github/workflows/unit-test.yml): on `pull_request` and `push`, Windows runner, .NET 7, `dotnet restore` → `dotnet build Contentstack.Net.sln` → `dotnet test` on `Contentstack.Core.Unit.Tests`. + +## CI — branch policy + +[`.github/workflows/check-branch.yml`](../../../.github/workflows/check-branch.yml): on `pull_request`, if base is `master` and head is not `staging`, the job fails with a message to open PRs from `staging` toward `master` per team policy. + +## CI — CodeQL + +[`.github/workflows/codeql-analysis.yml`](../../../.github/workflows/codeql-analysis.yml): static analysis on PRs (language matrix includes C# as configured in the workflow). + +## Release — NuGet + +[`.github/workflows/nuget-publish.yml`](../../../.github/workflows/nuget-publish.yml): triggered on **`release` `created`**. + +- `dotnet pack -c Release -o out` +- Push `contentstack.csharp.*.nupkg` to NuGet.org (`NUGET_API_KEY` / `NUGET_AUTH_TOKEN` secrets — names appear in workflow; values are org secrets). +- Secondary job may push to GitHub Packages (`nuget.pkg.github.com`). + +Maintainers: ensure `Directory.Build.props` version matches the release tag policy before publishing. + +## DocFX API docs + +Configuration: [`docfx_project/docfx.json`](../../../docfx_project/docfx.json). Output site: `_site/` under the docfx project. + +Prerequisite: [DocFX](https://dotnet.github.io/docfx/) CLI installed. + +```bash +cd docfx_project +docfx docfx.json +# or: docfx build docfx.json +``` + +If metadata `src` paths do not resolve (e.g. `src/**.csproj`), adjust `docfx.json` or project layout so metadata generation matches this repository’s structure. + +Related: [`filterRules.yml`](../../../docfx_project/filterRules.yml) for API filter rules. + +## Security / policy automation + +**SCA (dependencies):** [`.github/workflows/sca-scan.yml`](../../../.github/workflows/sca-scan.yml) — on PR, `dotnet restore`, then Snyk Dotnet action against `Contentstack.Core/obj/project.assets.json` (`SNYK_TOKEN` secret). Failures indicate vulnerable packages or scan misconfiguration. + +**Policy (repository hygiene):** [`.github/workflows/policy-scan.yml`](../../../.github/workflows/policy-scan.yml) — for public repos, checks presence of `SECURITY.md` (or `.github/SECURITY.md`) and a license file. Adjust repo contents if these jobs fail. + +When a PR fails these jobs, inspect the workflow log and fix dependencies or policy items as required by your team. + +## Projects (layout) + +| Path | Role | +|------|------| +| `Contentstack.Net.sln` | Main solution | +| `Contentstack.Core/` | Delivery SDK package | +| `Contentstack.AspNetCore/` | DI package | +| `Contentstack.Core.Unit.Tests/` | Unit tests | +| `Contentstack.Core.Tests/` | Integration tests | diff --git a/skills/error-handling/SKILL.md b/skills/error-handling/SKILL.md new file mode 100644 index 00000000..375ff5b3 --- /dev/null +++ b/skills/error-handling/SKILL.md @@ -0,0 +1,27 @@ +--- +name: error-handling +description: Error handling patterns for the Contentstack .NET SDK — ContentstackException hierarchy, domain-specific typed exceptions with static factory methods, GetContentstackError WebException parsing, centralized ErrorMessages.cs strings, and consumer catch order. Use when adding new exception types for a new domain area, modifying error messages, handling API HTTP errors, debugging WebException responses, or catching SDK errors in consumer code. +--- + +# Error Handling + +## When to use + +- New exceptions, new `ErrorMessages` strings, or HTTP error parsing. +- Reviewing catches in SDK internals or consumer apps. + +## Invariants + +- Throw **domain-specific** subclasses of `ContentstackException`, not raw `ContentstackException` for domain cases. +- **All user-facing message strings** live in **`ErrorMessages.cs`** — no inline strings in `throw`. +- On HTTP errors, use **`GetContentstackError`** and preserve **`ErrorCode`**, **`StatusCode`**, **`Errors`** when re-throwing. + +## Exception hierarchy (summary) + +`ContentstackException` → `QueryFilterException`, `AssetException`, `LivePreviewException`, `GlobalFieldException`, `EntryException`, `TaxonomyException`, `ContentTypeException` (see `Contentstack.Core/Internals/ContentstackExceptions.cs`). + +Base properties: `ErrorCode`, `StatusCode`, `Errors`. + +## Reference + +Exhaustive detail: **[references/error-patterns.md](references/error-patterns.md)** — `ErrorMessages` catalogue, API error JSON shape, HTTP mapping, `GetContentstackError` locations, factories, catch patterns, Live Preview / global field triggers. diff --git a/skills/error-handling/references/error-patterns.md b/skills/error-handling/references/error-patterns.md new file mode 100644 index 00000000..23dbaf92 --- /dev/null +++ b/skills/error-handling/references/error-patterns.md @@ -0,0 +1,274 @@ +# Error Patterns Reference + +## Complete ErrorMessages Catalogue + +All strings in `Contentstack.Core/Internals/ErrorMessages.cs`: + +### Query and Filter +```csharp +QueryFilterError = "Please provide valid params." +InvalidParamsError = "Invalid parameters provided. {0}" +``` + +### Asset +```csharp +AssetJsonConversionError = "Failed to convert asset JSON. Please check the asset format and data integrity." +AssetProcessingError = "An error occurred while processing the asset. {0}" +AssetLibraryRequestError = "Exception in {0}: {1}\nStackTrace: {2}" +``` + +### Entry +```csharp +EntryProcessingError = "An error occurred while processing the entry. {0}" +EntryUidRequired = "Please set entry uid." +EntryNotFoundInCache = "Entry is not present in cache" +``` + +### Global Field +```csharp +GlobalFieldIdNullError = "GlobalFieldId required. This value cannot be null or empty, define it in your configuration." +GlobalFieldProcessingError = "An error occurred while processing the globalField. {0}" +GlobalFieldQueryError = "Global field query failed. Check your query syntax and field schema before retrying." +``` + +### Live Preview +```csharp +LivePreviewTokenMissing = "Live Preview token missing. Add either a PreviewToken or a ManagementToken in the LivePreviewConfig." +``` + +### Client Request +```csharp +ContentstackClientRequestError = "Contentstack client request failed. Check your network settings or request parameters and try again: {0}" +ContentstackSyncRequestError = "An error occurred while processing the Contentstack client request: {0}" +``` + +### Taxonomy +```csharp +TaxonomyProcessingError = "An error occurred while processing the taxonomy operation: {0}" +``` + +### Content Type +```csharp +ContentTypeProcessingError = "Content type processing failed. Verify the schema and ensure all required fields are configured." +``` + +### Authentication and Configuration +```csharp +StackApiKeyRequired = "Stack api key can not be null." +AccessTokenRequired = "Access token can not be null." +EnvironmentRequired = "Environment can not be null." +AuthenticationNotPresent = "Authentication Not present." +ContentTypeNameRequired = "Please set contentType name." +``` + +### JSON and Parsing +```csharp +InvalidJsonFormat = "Please provide valid JSON." +ParsingError = "Parsing Error." +``` + +### Network and Server +```csharp +NoConnectionError = "Connection error" +ServerError = "Server interaction went wrong, Please try again." +NetworkUnavailable = "Network not available." +DefaultError = "Oops! Something went wrong. Please try again." +``` + +### Cache +```csharp +SavingNetworkCallResponseForCache = "Error while saving network call response." +``` + +### Initialization +```csharp +ContentstackDefaultMethodNotCalled = "You must called Contentstack.stack() first" +``` + +## CDA API Error Response Format + +The Contentstack CDA returns errors in this JSON format: + +```json +{ + "error_message": "The requested entry doesn't exist.", + "error_code": 141, + "errors": { + "field_name": ["validation message"] + } +} +``` + +`error_code` is Contentstack-specific (not HTTP status). Common codes: +- `141` — Entry not found +- `141` — Asset not found +- `109` — API key invalid +- `103` — Access token invalid +- `129` — Invalid query parameters + +## HTTP Status to Exception Mapping + +| HTTP Status | Typical cause | SDK behavior | +|------------|--------------|-------------| +| 400 | Invalid query params | `QueryFilterException` | +| 401 | Invalid API key / token | `ContentstackException` with StatusCode 401 | +| 404 | Entry / asset not found | `ContentstackException` with StatusCode 404 | +| 422 | Invalid field value | `ContentstackException` with `Errors` dict populated | +| 429 | Rate limit exceeded | `ContentstackException` with StatusCode 429 | +| 500 | Server error | `ContentstackException` with StatusCode 500 | + +## Where GetContentstackError Is Called + +The same static `GetContentstackError` method is replicated (acknowledged code smell) in: +- `Query` — wraps in `QueryFilterException` +- `Entry` — wraps in `EntryException` +- `ContentType` — wraps in `ContentTypeException` +- `Asset` — wraps in `AssetException` +- `AssetLibrary` — wraps in `AssetException` + +When adding a new model, follow the same pattern — copy `GetContentstackError` into the new class (or call the one from `Query` if same namespace/access level permits). + +## Exception with Errors Dictionary + +When the API returns field-level validation errors: + +```csharp +catch (ContentstackException ex) +{ + if (ex.Errors != null) + { + foreach (var field in ex.Errors) + { + // field.Key = field name, field.Value = error messages + Console.WriteLine($"Field '{field.Key}': {field.Value}"); + } + } +} +``` + +## LivePreviewException Trigger Conditions + +Thrown when `LivePreviewConfig.Enable = true` but neither `ManagementToken` nor `PreviewToken` is configured: + +```csharp +if (livePreviewConfig.Enable) +{ + if (string.IsNullOrEmpty(livePreviewConfig.ManagementToken) + && string.IsNullOrEmpty(livePreviewConfig.PreviewToken)) + throw new LivePreviewException(); // uses LivePreviewTokenMissing message +} +``` + +## GlobalFieldException.CreateForIdNull Trigger + +Thrown when `GlobalField(null)` or `GlobalField("")` is called — validated before any HTTP request: + +```csharp +public GlobalField GlobalField(string uid) +{ + if (string.IsNullOrEmpty(uid)) + throw GlobalFieldException.CreateForIdNull(); + // ... +} +``` + +## Adding Messages for New Features — Checklist + +1. Add `public const string` to the appropriate section in `ErrorMessages.cs` +2. Use `{0}` placeholder for `string.Format` when appending exception details +3. Add the factory method on the exception class using `ErrorMessages.FormatExceptionDetails(ex)` for processing errors +4. Never concatenate exception details manually — always use `FormatExceptionDetails()` + +--- + +## Static factory pattern (examples) + +Use factories — not `new XyzException("literal")`: + +```csharp +throw QueryFilterException.Create(innerException); +throw GlobalFieldException.CreateForIdNull(); +throw AssetException.CreateForJsonConversionError(); +throw AssetException.CreateForProcessingError(innerException); +throw EntryException.CreateForProcessingError(innerException); +throw TaxonomyException.CreateForProcessingError(innerException); +throw ContentTypeException.CreateForProcessingError(innerException); +``` + +## Adding a new domain exception + +1. Add the class in `ContentstackExceptions.cs` extending `ContentstackException` with static factories. +2. Add `public const string` entries in `ErrorMessages.cs`; use `{0}` when wrapping `FormatExceptionDetails(innerException)`. + +Example skeleton: + +```csharp +public class MyFeatureException : ContentstackException +{ + public MyFeatureException(string message) : base(message) { } + public MyFeatureException(string message, Exception innerException) : base(message, innerException) { } + + public static MyFeatureException CreateForProcessingError(Exception innerException) + { + return new MyFeatureException( + string.Format(ErrorMessages.MyFeatureProcessingError, + ErrorMessages.FormatExceptionDetails(innerException)), + innerException); + } +} +``` + +## GetContentstackError (implementation sketch) + +`WebException` responses are parsed into `ContentstackException` with `ErrorCode`, `StatusCode`, and `Errors`. The same helper pattern is duplicated on `Query`, `Entry`, `ContentType`, `Asset`, `AssetLibrary` — follow the existing copy when adding a new HTTP-calling model. + +```csharp +internal static ContentstackException GetContentstackError(Exception ex) +{ + var webEx = (WebException)ex; + using var stream = webEx.Response.GetResponseStream(); + string errorMessage = new StreamReader(stream).ReadToEnd(); + JObject data = JObject.Parse(errorMessage); + int errorCode = data["error_code"]?.Value() ?? 0; + HttpStatusCode statusCode = ((HttpWebResponse)webEx.Response).StatusCode; + var errors = data["errors"]?.ToObject>(); + return new ContentstackException(data["error_message"]?.Value()) + { + ErrorCode = errorCode, + StatusCode = statusCode, + Errors = errors + }; +} +``` + +## Standard catch block (SDK internals) + +Preserve `ErrorCode`, `StatusCode`, and `Errors` when re-throwing domain exceptions: + +```csharp +catch (Exception ex) +{ + ContentstackException error = GetContentstackError(ex); + throw new QueryFilterException(error.Message, ex) + { + ErrorCode = error.ErrorCode, + StatusCode = error.StatusCode, + Errors = error.Errors + }; +} +``` + +## Consumer catch order (example) + +```csharp +try { /* await query.Find() */ } +catch (QueryFilterException ex) { /* query validation */ } +catch (ContentstackException ex) { /* API / HTTP */ } +catch (Exception ex) { /* network / unknown */ } +``` + +## ErrorMessages.FormatExceptionDetails + +```csharp +ErrorMessages.FormatExceptionDetails(innerException) +``` diff --git a/skills/models-and-serialization/SKILL.md b/skills/models-and-serialization/SKILL.md new file mode 100644 index 00000000..723f2f4e --- /dev/null +++ b/skills/models-and-serialization/SKILL.md @@ -0,0 +1,23 @@ +--- +name: models-and-serialization +description: Model and serialization patterns for the Contentstack .NET SDK — Entry/Asset shape, catch-all Object dictionary, generic Fetch/Find projections, CSJsonConverter attribute-driven converter registration, EntryJsonConverter/AssetJsonConverter, ContentstackCollection, JsonProperty for API name mismatches, entry variants. Use when adding new model types, writing JSON converters, working with entry variants, embedded assets, or modifying serialization settings. +--- + +# Models and Serialization + +## When to use + +- New models, converters, or `JsonProperty` mappings. +- Entry variants, embedded references, RTE/modular blocks behavior. +- Tuning `SerializerSettings`. + +## Invariants + +- CDA JSON is **snake_case** — use **`[JsonProperty("snake_case")]`** on mapped properties; do not rely on default PascalCase naming. +- Custom converters: **`[CSJsonConverter("Name")]`** on the model + **`JsonConverter`** implementation in **`Contentstack.Core/Internals/`**. +- New converters are registered automatically at client init (see reference). +- Use **`Newtonsoft.Json`** only — not `System.Text.Json`. + +## Reference + +**[references/serialization-patterns.md](references/serialization-patterns.md)** — consumer patterns (Entry/Asset, Fetch/Find, JsonProperty, variants, new models, SerializerSettings), converter internals, `parseJObject`, RTE/modular blocks, collection edge cases. diff --git a/skills/models-and-serialization/references/serialization-patterns.md b/skills/models-and-serialization/references/serialization-patterns.md new file mode 100644 index 00000000..dc80c340 --- /dev/null +++ b/skills/models-and-serialization/references/serialization-patterns.md @@ -0,0 +1,228 @@ +# Serialization Patterns Reference + +## CSJsonConverter Registration Flow + +At `ContentstackClient` construction time: + +``` +1. Scan all assemblies in current AppDomain +2. Find all types with [CSJsonConverter("ConverterName")] attribute +3. Find the JsonConverter class with matching name in Contentstack.Core.Internals +4. Instantiate it and add to SerializerSettings.Converters +5. All subsequent Fetch/Find calls use these converters +``` + +This means **converter registration is automatic** — adding the attribute and converter class is all that's required. + +## EntryJsonConverter Internals + +`EntryJsonConverter` handles the nested entry JSON structure from the CDA: + +```json +{ + "uid": "blt123", + "title": "My Entry", + "publish_details": { "environment": "production", "locale": "en-us", "time": "...", "user": "..." }, + "locale": "en-us", + "_metadata": { ... }, + "custom_field": "value", + "reference_field": [{ "uid": "blt456", "_content_type_uid": "blog" }] +} +``` + +The converter: +1. Reads the raw `JObject` +2. Maps known fields to strongly-typed properties (`Uid`, `Title`, `PublishDetails`, etc.) +3. Puts all remaining fields into `entry.Object` (the catch-all dictionary) + +## AssetJsonConverter Internals + +Similar pattern for assets — maps `uid`, `title`, `url`, `content_type`, `file_size`, `filename`, `tags` to typed properties; remaining fields to `asset.Object`. + +## parseJObject\ in Query + +After `HttpRequestHandler.ProcessRequest` returns a JSON string, `Query.parseJObject` does: + +```csharp +JObject jObject = JObject.Parse(responseString); + +// For entry queries +JArray entries = (JArray)jObject["entries"]; +collection.Items = entries.ToObject>(client.Serializer); +collection.Count = jObject["count"]?.Value() ?? 0; +collection.Skip = (int)UrlQueries.GetValueOrDefault("skip", 0); +collection.Limit = (int)UrlQueries.GetValueOrDefault("limit", 100); +``` + +The `"entries"` token name is fixed by the CDA response format. Asset library uses `"assets"`. + +## JsonProperty Mapping Reference + +Key mappings already in the codebase: + +| JSON field | C# property | Model | +|-----------|-------------|-------| +| `publish_details` | `PublishDetails` | `Entry` | +| `_metadata` | `Metadata` / `_metadata` | `Entry` | +| `content_type` | `ContentType` | `Asset` | +| `file_size` | `FileSize` | `Asset` | +| `filename` | `FileName` | `Asset` | +| `created_at` | `CreatedAt` | various | +| `updated_at` | `UpdatedAt` | various | +| `created_by` | `CreatedBy` | various | + +## Newtonsoft.Json Settings Defaults + +The SDK uses default `JsonSerializerSettings` with no special configuration unless consumers override `client.SerializerSettings`. This means: +- `NullValueHandling.Include` (nulls are included) +- No date format override (ISO 8601 by default) +- No contract resolver override (property names as-is, so `[JsonProperty]` is required) +- No type name handling + +## Handling Embedded RTE Items + +RTE (Rich Text Editor) fields with embedded entries/assets are processed via `contentstack.utils` NuGet package. The `Entry` model surfaces the raw RTE JSON; consumers call the utils library to resolve embedded references: + +```csharp +// RTE field value from entry.Object["rte_field"] +// Pass to contentstack.utils for resolution +Utils.RenderContent(content, entryEmbeds); +``` + +## Deep Reference Deserialization + +When `IncludeReference()` is called, the CDA returns nested objects inside the entry JSON. These are deserialized as nested dictionaries in `entry.Object` or as typed sub-objects when the consumer POCO uses `[JsonProperty]` on the reference field: + +```csharp +public class BlogPost +{ + [JsonProperty("author")] + public Author Author { get; set; } // auto-deserialized if CDA returns expanded ref +} +``` + +If the reference is not expanded (not included), it will be an array of `{"uid": "...", "_content_type_uid": "..."}` objects. + +## Modular Blocks Deserialization + +Modular blocks are returned as JSON arrays of objects, each with a `_content_type_uid` discriminator: + +```json +"modular_blocks": [ + { "block_a": { "field1": "value" } }, + { "block_b": { "field2": "value" } } +] +``` + +Map with a `List>` or use a custom converter with a discriminator switch on the first key. + +## ContentstackCollection Parsing Edge Cases + +- `count` field only present when `IncludeCount()` is set on the query +- `entries` array is present even when empty (`[]`), never `null` +- `skip` and `limit` in the response may differ from what was requested if the CDA has its own limits + +--- + +## Entry model shape (quick reference) + +```csharp +// Strongly-typed fields (typical) +entry.Uid; entry.Title; entry.Tags; entry.Metadata; entry.PublishDetails; + +// Catch-all for arbitrary content type fields +entry.Object // Dictionary +``` + +```csharp +var price = entry.Object["price"]; +var color = entry.Object["color"] as string; +``` + +## Fetch and Find with typed POCOs + +Prefer typed models over `entry.Object` for structured access: + +```csharp +public class Product +{ + [JsonProperty("title")] + public string Title { get; set; } + + [JsonProperty("price")] + public decimal Price { get; set; } + + [JsonProperty("uid")] + public string Uid { get; set; } +} + +var result = await client.ContentType("product").Query().Find(); +var product = await client.ContentType("product").Entry("uid").Fetch(); +``` + +Deserialization uses `client.Serializer` (from `client.SerializerSettings`). + +## JsonProperty — always map snake_case + +The CDA uses `snake_case`. Newtonsoft defaults to PascalCase property names. Always annotate: + +```csharp +[JsonProperty("publish_details")] +public object PublishDetails { get; set; } +``` + +Without `[JsonProperty]`, deserialization looks for the wrong JSON keys. + +## CSJsonConverter on models + +```csharp +[CSJsonConverter("EntryJsonConverter")] +public class Entry { ... } +``` + +Converters live in `Contentstack.Core/Internals/`. See [CSJsonConverter Registration Flow](#csjsonconverter-registration-flow). + +## ContentstackCollection shape + +```csharp +public class ContentstackCollection +{ + public IEnumerable Items { get; } + public int Count { get; } + public int Skip { get; } + public int Limit { get; } +} +``` + +Parsed from `"entries"` or `"assets"` in `Query.parseJObject` (see [parseJObject\ in Query](#parsejobjectt-in-query)). + +## Asset model (common properties) + +`Uid`, `Title`, `Url`, `ContentType`, `FileSize`, `FileName`, `Tags`, plus `Object` for other fields. `AssetJsonConverter` handles nested JSON; `AssetLibrary.FetchAll()` returns `ContentstackCollection`. + +## Entry variants + +```csharp +entry.SetVariant("variant_uid"); +var result = await entry.Fetch(); +``` + +Uses `_variant` and the `x-cs-variant-uid` header path as implemented on `Entry`. + +## Adding a new model type (checklist) + +1. Add class under `Contentstack.Core/Models/` with `[JsonProperty]` on API fields and optional `Object` catch-all. +2. If custom deserialization is required, add `internal class MyModelJsonConverter : JsonConverter` in `Internals/`, implement `ReadJson` / `WriteJson` as needed (delivery is typically read-heavy). +3. Mark the model with `[CSJsonConverter("MyModelJsonConverter")]`. +4. Expose via `ContentstackClient` factory methods if it is a first-class API surface. + +## SerializerSettings customization + +Consumers may adjust before calls: + +```csharp +client.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; +client.SerializerSettings.DateFormatString = "yyyy-MM-dd"; +``` + +`Serializer` is rebuilt from `SerializerSettings` when needed. diff --git a/skills/query-building/SKILL.md b/skills/query-building/SKILL.md new file mode 100644 index 00000000..bed4e9a5 --- /dev/null +++ b/skills/query-building/SKILL.md @@ -0,0 +1,35 @@ +--- +name: query-building +description: Query building patterns for the Contentstack .NET SDK — two-dictionary architecture (QueryValueJson + UrlQueries), Mongo-style operators, fluent chaining, Exec() merge order, pagination (skip/limit), sync API pagination tokens, taxonomy query path. Use when adding query operators or filters, modifying Query.cs, working on pagination, debugging query parameter serialization, or building entry/asset queries. +--- + +# Query Building + +## When to use + +- Adding or changing operators on `Query`, `AssetLibrary`, or related fluent APIs. +- Debugging why a filter or URL param is wrong. +- Pagination, sync, or taxonomy entry queries. + +## Invariants + +- Field-level Mongo filters live in **`QueryValueJson`**; top-level API params (locale, skip, limit, includes, etc.) live in **`UrlQueries`**. +- **`Exec()`** merges `environment`, optional live-preview keys, then `query` (from `QueryValueJson`), then each `UrlQueries` entry—later keys override earlier ones. +- New filter methods return **`this`** and validate keys with **`QueryFilterException`** where appropriate. +- Never set **`environment`** manually in `UrlQueries`—`Exec()` injects it from `Config`. + +## Exec() merge order (compact) + +```csharp +mainJson["environment"] = stack.Config.Environment; +// live preview keys if enabled +if (QueryValueJson.Count > 0) + mainJson["query"] = QueryValueJson; +foreach (var kvp in UrlQueries) + mainJson[kvp.Key] = kvp.Value; +// → HttpRequestHandler.ProcessRequest(..., BodyJson) +``` + +## Reference + +All exhaustive patterns, tables, and examples: **[references/query-patterns.md](references/query-patterns.md)** (quick-reference tables, extending `Query.cs`, terminal operations, pagination, sync, taxonomy URL paths, asset library, common mistakes). diff --git a/skills/query-building/references/query-patterns.md b/skills/query-building/references/query-patterns.md new file mode 100644 index 00000000..ec0753ef --- /dev/null +++ b/skills/query-building/references/query-patterns.md @@ -0,0 +1,280 @@ +# Query Patterns Reference + +## Quick reference: QueryValueJson operators + +| Method | Operator | Example JSON shape | +|--------|----------|-------------------| +| `Where(key, value)` | direct equality | `{"field": "value"}` | +| `NotEqualTo(key, value)` | `$ne` | `{"field":{"$ne":"x"}}` | +| `ContainedIn(key, values)` | `$in` | `{"field":{"$in":["a","b"]}}` | +| `NotContainedIn(key, values)` | `$nin` | `{"field":{"$nin":["a"]}}` | +| `Exists(key)` | `$exists: true` | `{"field":{"$exists":true}}` | +| `NotExists(key)` | `$exists: false` | `{"field":{"$exists":false}}` | +| `GreaterThan(key, value)` | `$gt` | `{"field":{"$gt":10}}` | +| `LessThan(key, value)` | `$lt` | `{"field":{"$lt":10}}` | +| `Regex(key, pattern)` | `$regex` | `{"field":{"$regex":"^blt"}}` | +| `And(queries[])` | `$and` | array of query objects | +| `Or(queries[])` | `$or` | array of query objects | + +## Quick reference: UrlQueries keys + +| Method | Key | Notes | +|--------|-----|-------| +| `SetLocale(locale)` | `locale` | prefer over obsolete `Language` enum | +| `Skip(n)` | `skip` | pagination offset | +| `Limit(n)` | `limit` | pagination page size | +| `IncludeSchema()` | `include_schema` | `true` | +| `IncludeCount()` | `include_count` | `true` | +| `Tags(tags[])` | `tags` | `string[]` → repeated param | +| `Only(fields[])` | `only[BASE][]` | field projection | +| `Except(fields[])` | `except[BASE][]` | field exclusion | +| `IncludeReference(key)` | `include[]` | reference expansion | + +## Extending Query.cs (new fluent methods) + +### New Mongo-style filter operator + +```csharp +// Pattern used by all existing operators in Query.cs +public Query MyNewOperator(string key, object value) +{ + if (key == null) + throw QueryFilterException.Create(new ArgumentNullException(nameof(key))); + + if (!QueryValueJson.ContainsKey(key)) + QueryValueJson[key] = new Dictionary(); + + ((Dictionary)QueryValueJson[key])["$myop"] = value; + return this; // always return this for fluent chaining +} +``` + +### New URL-level parameter + +```csharp +public Query MyParam(string value) +{ + UrlQueries["my_param"] = value; + return this; +} +``` + +## Terminal operations + +```csharp +Task> result = await query.Find(); +Task result = await query.FindOne(); +Task count = await query.Count(); +``` + +## Paginating Find results (entries) + +```csharp +// ContentstackCollection response shape +result.Items // IEnumerable +result.Count // total count (requires IncludeCount()) +result.Skip // current offset +result.Limit // current page size + +// Paging loop +int skip = 0, limit = 100; +ContentstackCollection page; +do { + page = await query.Skip(skip).Limit(limit).Find(); + // process page.Items + skip += limit; +} while (page.Items.Count() == limit); +``` + +## SyncStack short reference + +When using `SyncRecursive` / sync APIs: + +```csharp +SyncStack syncResult = await client.SyncRecursive(parameters); +// SyncStack.PaginationToken — non-null while more pages exist +// SyncStack.SyncToken — final token for next delta sync +``` + +(See [Sync API Patterns](#sync-api-patterns) below for full sync flows.) + +## Complete Operator Categories + +### Comparison Operators + +```csharp +query.Where("title", "My Entry") // direct equality +query.NotEqualTo("status", "draft") // $ne +query.GreaterThan("price", 100) // $gt +query.GreaterThanEqualTo("price", 100) // $gte +query.LessThan("price", 200) // $lt +query.LessThanEqualTo("price", 200) // $lte +``` + +### Array / Set Operators + +```csharp +query.ContainedIn("color", new[] {"red", "blue"}) // $in +query.NotContainedIn("color", new[] {"green"}) // $nin +``` + +### Existence Operators + +```csharp +query.Exists("field_name") // $exists: true +query.NotExists("field_name") // $exists: false +``` + +### String Operators + +```csharp +query.Regex("uid", "^blt[a-zA-Z0-9]+$") // $regex +query.Regex("title", "^hello", "i") // $regex with $options modifier +``` + +### Logical Operators + +```csharp +// And / Or take Query[] — each sub-query builds its own QueryValueJson +var q1 = client.ContentType("ct").Query().Where("color", "red"); +var q2 = client.ContentType("ct").Query().Where("size", "large"); +query.And(new[] { q1, q2 }); // $and +query.Or(new[] { q1, q2 }); // $or +``` + +## Reference / Include Patterns + +```csharp +query.IncludeReference("reference_field"); // expand single reference +query.IncludeReference(new[] {"ref1", "ref2"}); // expand multiple +query.IncludeSchema(); // include content type schema +query.IncludeCount(); // include total count in response +query.IncludeOwner(); // include entry owner info +query.IncludeMetadata(); // include entry metadata +``` + +## Field Projection + +```csharp +query.Only(new[] {"title", "uid", "price"}); // return only these fields +query.Except(new[] {"body", "image"}); // exclude these fields + +// For referenced fields +query.OnlyWithReferenceUid(new[] {"title"}, "reference_field"); +query.ExceptWithReferenceUid(new[] {"body"}, "reference_field"); +``` + +## Ordering + +```csharp +query.OrderByAscending("title"); +query.OrderByDescending("created_at"); +``` + +## Locale and Environment + +```csharp +query.SetLocale("en-us"); // preferred — string locale code +// Environment is injected automatically from Config in Exec() +// Never set "environment" manually in UrlQueries +``` + +## Exec() Implementation Detail + +The full merge performed in `Query.Exec()` before calling `HttpRequestHandler`: + +```csharp +var mainJson = new Dictionary(); + +// 1. Environment (always injected from Config) +mainJson["environment"] = ContentTypeInstance.StackInstance.Config.Environment; + +// 2. Live preview headers (if enabled and content type matches) +if (livePreviewActive) +{ + mainJson["live_preview"] = livePreviewConfig.LivePreview; + mainJson["authorization"] = livePreviewConfig.ManagementToken; + // or mainJson["preview_token"] = livePreviewConfig.PreviewToken; +} + +// 3. Mongo-style query filter (only if non-empty) +if (QueryValueJson.Count > 0) + mainJson["query"] = QueryValueJson; + +// 4. All UrlQueries (locale, skip, limit, includes, projections, etc.) +foreach (var kvp in UrlQueries) + mainJson[kvp.Key] = kvp.Value; +``` + +## How QueryValueJson Is Serialized + +`Dictionary` values are JSON-serialized by `HttpRequestHandler`: + +``` +QueryValueJson = { "title": {"$ne": "Draft"}, "color": {"$in": ["red","blue"]} } +→ query={"title":{"$ne":"Draft"},"color":{"$in":["red","blue"]}} +``` + +This becomes a single `query=` URL parameter with the JSON as its value. + +## Sync API Patterns + +```csharp +// Initial sync (all published content) +SyncStack result = await client.Sync(new SyncStack() { Type = SyncType.entry_published }); + +// Paginated initial sync +SyncStack result = await client.SyncRecursive(parameters); +// result.PaginationToken — continue paginating if not null +// result.SyncToken — use for next delta sync + +// Delta sync (changes since last sync) +SyncStack delta = await client.SyncToken(result.SyncToken); + +// Manual pagination loop +SyncStack page = initialResult; +while (page.PaginationToken != null) +{ + page = await client.SyncPaginationToken(page.PaginationToken); + // process page.Items +} +``` + +## Taxonomy Query Patterns + +```csharp +// Create taxonomy query via client.Taxonomies() +Query taxonomyQuery = client.Taxonomies(); + +// Same filter methods apply +taxonomyQuery.Where("taxonomies.animals", new Dictionary { + { "$eq", "mammals" } +}); + +var results = await taxonomyQuery.Find(); +``` + +**URL paths** + +- Normal content-type query: `{stack.Config.BaseUrl}/content_types/{uid}/entries` +- Taxonomy query (`client.Taxonomies()`): `{stack.Config.BaseUrl}/taxonomies/entries` + +Filter and pagination APIs are the same; only the base path differs. + +## AssetLibrary Query Patterns + +```csharp +AssetLibrary assetLib = client.Assets(); +assetLib.Skip(0).Limit(100); +assetLib.IncludeCount(); +assetLib.SetLocale("en-us"); +ContentstackCollection assets = await assetLib.FetchAll(); +``` + +## Common Mistakes to Avoid + +- Never set `"environment"` key manually in `UrlQueries` — it is always injected from `Config.Environment` in `Exec()` +- Never call `ProcessRequest` directly — always go through model methods (`Find`, `Fetch`, etc.) +- Never modify `QueryValueJson` from outside `Query` — use the public fluent methods +- `And()` / `Or()` take full `Query` instances, not raw dictionaries +- `string[]` values in `UrlQueries` become repeated URL params, not JSON arrays — use for tags, not Mongo operators diff --git a/skills/sdk-core-patterns/SKILL.md b/skills/sdk-core-patterns/SKILL.md new file mode 100644 index 00000000..8a3ebeff --- /dev/null +++ b/skills/sdk-core-patterns/SKILL.md @@ -0,0 +1,54 @@ +--- +name: sdk-core-patterns +description: Core architecture of the Contentstack .NET Delivery SDK — namespaces, ContentstackClient factory, Config/ContentstackOptions, HttpRequestHandler (HttpWebRequest GET), plugin hooks, ASP.NET Core DI, and multi-targeting. Use when working on new SDK features, adding model types, wiring DI, understanding request flow, or onboarding to the codebase. +--- + +# SDK Core Patterns + +## When to use + +- Onboarding, request flow, or where types live. +- Changing HTTP behavior, config, plugins, or DI. +- Anything that must go through `HttpRequestHandler`. + +## Namespace map + +| Namespace | Purpose | +|-----------|---------| +| `Contentstack.Core` | `ContentstackClient` — root entry point | +| `Contentstack.Core.Models` | `Query`, `Entry`, `Asset`, `AssetLibrary`, `ContentType`, `GlobalField`, `GlobalFieldQuery`, `Taxonomy`, `SyncStack`, `ContentstackCollection` | +| `Contentstack.Core.Configuration` | `ContentstackOptions`, `Config` (internal), `LivePreviewConfig` | +| `Contentstack.Core.Internals` | `HttpRequestHandler`, exceptions, enums, converters, constants — all `internal` | +| `Contentstack.Core.Interfaces` | `IContentstackPlugin` | +| `Contentstack.AspNetCore` | `IServiceCollectionExtensions` for DI registration | + +## Invariants + +- **`ContentstackClient`** is the only public entry point for creating stack operations. +- **All HTTP** goes through **`HttpRequestHandler.ProcessRequest`** — no `HttpClient`, no bypassing for feature work. +- **GET + query string** for requests; plugins run `OnRequest` / `OnResponse` around the call. +- **Multi-target:** `netstandard2.0`, `net47`, `net472` — no `System.Text.Json`; Newtonsoft.Json throughout. +- **Version:** `Directory.Build.props` only — do not set `` in individual `.csproj` files. + +## ContentstackClient (compact) + +```csharp +var client = new ContentstackClient(options); + +client.ContentType("uid").Query().Find(); +client.ContentType("uid").Entry("uid"); +client.Assets(); client.Asset("uid"); +client.GlobalField("uid"); client.GlobalFields(); +client.Taxonomies(); client.Sync(...); +``` + +## Options → Config → BaseUrl + +`ContentstackOptions` → internal `Config` → `BaseUrl` (region + host + version). Required: `ApiKey`, `DeliveryToken`, `Environment`. See **[SDK Architecture](references/sdk-architecture.md)** for region table, live preview host switching, and internal client state. + +## Reference map + +| Topic | Document | +|-------|----------| +| Full HTTP flow, Config/BaseUrl, live preview, query serialization, plugins, regions, NuGet layout | [references/sdk-architecture.md](references/sdk-architecture.md) | +| `AddContentstack`, `ContentstackOptions` section, transient registration | [references/aspnetcore-integration.md](references/aspnetcore-integration.md) | diff --git a/skills/sdk-core-patterns/references/aspnetcore-integration.md b/skills/sdk-core-patterns/references/aspnetcore-integration.md new file mode 100644 index 00000000..73ca887a --- /dev/null +++ b/skills/sdk-core-patterns/references/aspnetcore-integration.md @@ -0,0 +1,48 @@ +# ASP.NET Core integration + +Source: [`Contentstack.AspNetCore/IServiceCollectionExtensions.cs`](../../../Contentstack.AspNetCore/IServiceCollectionExtensions.cs). + +## Registration + +Two overloads register the same services: + +```csharp +public static IServiceCollection AddContentstack(this IServiceCollection services, IConfigurationRoot configuration) +public static IServiceCollection AddContentstack(this IServiceCollection services, IConfiguration configuration) +``` + +Both: + +1. `services.AddOptions()` +2. `services.Configure(configuration.GetSection("ContentstackOptions"))` +3. `services.TryAddTransient()` + +## Configuration section + +Bind options from configuration using section name **`ContentstackOptions`**: + +```json +{ + "ContentstackOptions": { + "ApiKey": "...", + "DeliveryToken": "...", + "Environment": "..." + } +} +``` + +Adjust property names to match [`ContentstackOptions`](../../../Contentstack.Core/Configuration/ContentstackOptions.cs) public properties. + +## Service lifetime + +`ContentstackClient` is registered as **transient** (`TryAddTransient`). Each resolution gets a new instance; use this when injecting into short-lived scopes or when the app expects a fresh client per operation. + +## Usage in app code + +Inject `ContentstackClient` or `IOptions` as needed after calling `AddContentstack` in `Program.cs` / `Startup.cs`: + +```csharp +services.AddContentstack(configuration); +``` + +Ensure `configuration` includes the `ContentstackOptions` section (e.g. `appsettings.json`, environment variables, user secrets). diff --git a/skills/sdk-core-patterns/references/sdk-architecture.md b/skills/sdk-core-patterns/references/sdk-architecture.md new file mode 100644 index 00000000..8eb5bde5 --- /dev/null +++ b/skills/sdk-core-patterns/references/sdk-architecture.md @@ -0,0 +1,143 @@ +# SDK Architecture Reference + +## Full Request Flow + +``` +ContentstackClient + └── ContentType("uid") → ContentType + └── Query() → Query + └── Find() → Query.Exec() + └── HttpRequestHandler.ProcessRequest(url, headers, bodyJson) + ├── Serialize BodyJson → query string + ├── Create HttpWebRequest (GET) + ├── Set headers (api_key, access_token, branch, x-user-agent) + ├── foreach plugin: OnRequest(client, request) + ├── await request.GetResponseAsync() + ├── foreach plugin: OnResponse(client, request, response, body) + └── return JSON string → parsed in Query.parseJObject +``` + +## Config.BaseUrl Composition + +``` +Protocol Region Code Host Version +"https://" "" "cdn.contentstack.io" "/v3" → US (default) +"https://" "eu-" "cdn.contentstack.com" "/v3" → EU +"https://" "azure-na-" "cdn.contentstack.com" "/v3" → AZURE_NA +"https://" "azure-eu-" "cdn.contentstack.com" "/v3" → AZURE_EU +"https://" "gcp-na-" "cdn.contentstack.com" "/v3" → GCP_NA +"https://" "au-" "cdn.contentstack.com" "/v3" → AU +``` + +`HostURL` defaults to `cdn.contentstack.io` for US, `cdn.contentstack.com` for all other regions. + +## Live Preview URL Resolution + +When `LivePreviewConfig.Enable == true` and `LivePreview != "init"` and `ContentTypeUID` matches the queried content type, `Config.getBaseUrl()` returns the live preview host instead of `BaseUrl`: + +``` +"https://{livePreviewConfig.Host}/{version}" +``` + +Additional headers injected: `live_preview`, `authorization` (management token) or `preview_token`, optional `release_id`, `preview_timestamp`. + +## Query String Serialization Rules (HttpRequestHandler) + +| Value type | Serialization | +|-----------|--------------| +| `string` | `key=value` | +| `string[]` | `key=v1&key=v2` (repeated) | +| `Dictionary` | `key={"$in":["a","b"]}` (JSON) | +| Other | `key=value.ToString()` | + +## ContentstackClient Internal State + +```csharp +internal string StackApiKey // from options +internal Dictionary _Headers // api_key, access_token/delivery_token +internal Dictionary _StackHeaders // shared across requests +internal LivePreviewConfig LivePreviewConfig // null if not configured +public List Plugins // empty by default +public JsonSerializerSettings SerializerSettings // for Fetch/Find +internal JsonSerializer Serializer // created from SerializerSettings +``` + +## How Models Get Stack Context + +All model constructors are `internal`. `ContentstackClient` methods set back-references: + +```csharp +// ContentType.cs internal wiring +internal ContentstackClient StackInstance { get; set; } + +// Query.cs +private ContentType ContentTypeInstance { get; set; } // for entry path +private ContentstackClient TaxonomyInstance { get; set; } // for taxonomy path +``` + +Models build their URL from `ContentTypeInstance.StackInstance.Config.BaseUrl` at call time (lazy). + +## Plugin Implementation Pattern + +```csharp +public class MyPlugin : IContentstackPlugin +{ + public Task OnRequest(ContentstackClient stack, HttpWebRequest request) + { + // Mutate request (add headers, log, etc.) + return Task.FromResult(request); + } + + public Task OnResponse(ContentstackClient stack, HttpWebRequest request, + HttpWebResponse response, string responseString) + { + // Inspect/transform response body string + return Task.FromResult(responseString); + } +} + +// Register +client.Plugins.Add(new MyPlugin()); +``` + +## ContentstackRegion Enum Values + +```csharp +public enum ContentstackRegion { US, EU, AZURE_NA, AZURE_EU, GCP_NA, AU } +``` + +`ContentstackRegionCode` (internal enum) maps to URL prefixes: `eu`, `azure_na`, `azure_eu`, `gcp_na`, `au`. Underscores are replaced with hyphens in the URL. + +## Key NuGet Dependencies (Contentstack.Core.csproj) + +| Package | Version | Purpose | +|---------|---------|---------| +| `Newtonsoft.Json` | 13.0.4 | All JSON serialization | +| `Microsoft.Extensions.Options` | 8.0.2 | `IOptions` | +| `Markdig` | 0.36.2 | Markdown processing in RTE fields | +| `contentstack.utils` | 1.0.6 | RTE embedded item resolution | + +## Solution Layout + +``` +Contentstack.Net.sln +├── Contentstack.Core/ ← Main SDK package (contentstack.csharp on NuGet) +├── Contentstack.AspNetCore/ ← DI extension (contentstack.aspnetcore on NuGet) +├── Contentstack.Core.Tests/ ← Integration tests (net7.0, hits live API) +└── Contentstack.Core.Unit.Tests/ ← Unit tests (no network) +``` + +Version shared via `Directory.Build.props` → `2.26.0` (or current). + +## Supporting internals (maintainers) + +When debugging HTTP, serialization edges, or multi-target behavior, these types in `Contentstack.Core/Internals/` are often involved. They are not public API. + +| Area | Types / files | +|------|----------------| +| Async helpers around `HttpWebRequest` | `WebRequestAsyncExtensions.cs` | +| Version string / user-agent composition | `VersionUtility.cs`, `StackOutput.cs` | +| JSON / value coercion helpers | `ContentstackConvert.cs` | +| Language / locale enums | `LanguageEnums.cs` | + +Prefer changing behavior through `HttpRequestHandler`, `Config`, and public models rather than exposing these types. diff --git a/skills/testing/SKILL.md b/skills/testing/SKILL.md new file mode 100644 index 00000000..0f9c278a --- /dev/null +++ b/skills/testing/SKILL.md @@ -0,0 +1,28 @@ +--- +name: testing +description: Testing patterns for the Contentstack .NET SDK — unit tests using AutoFixture and private field reflection (no network), and integration tests using IntegrationTestBase, TestDataHelper, LogArrange/LogAct/LogAssert, xUnit traits, and RequestLoggingPlugin. Use when writing new tests, adding coverage for a new feature, debugging integration test failures, or understanding the test structure. +--- + +# Testing + +## When to use + +- New unit tests for `Query` or models (reflection on `QueryValueJson` / `UrlQueries`). +- New or failing integration tests against the live API. + +## Projects + +| Project | Framework | Purpose | +|---------|-----------|---------| +| `Contentstack.Core.Unit.Tests` | xUnit + AutoFixture | No network; assert internal state via reflection | +| `Contentstack.Core.Tests` | xUnit, net7.0 | Live API; requires `app.config` (or equivalent) credentials | + +## Invariants + +- Unit tests: **no real network** — use AutoFixture for options, reflection for private dictionaries. +- Integration tests: **never commit secrets** — credentials from `app.config` / env locally. +- New `Query` methods: unit test operators + null/invalid cases; integration test when behavior is API-bound. + +## Reference + +Templates, reflection helpers, `IntegrationTestBase`, `TestDataHelper`, mocks, traits, `app.config` keys, dotnet CLI filters, RequestLoggingPlugin, coverage guidelines: **[references/testing-patterns.md](references/testing-patterns.md)**. diff --git a/skills/testing/references/testing-patterns.md b/skills/testing/references/testing-patterns.md new file mode 100644 index 00000000..d83505da --- /dev/null +++ b/skills/testing/references/testing-patterns.md @@ -0,0 +1,309 @@ +# Testing Patterns Reference + +## Complete Unit Test Template + +```csharp +using System.Collections.Generic; +using System.Reflection; +using AutoFixture; +using Contentstack.Core; +using Contentstack.Core.Configuration; +using Contentstack.Core.Models; +using Contentstack.Core.Internals; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Contentstack.Core.Unit.Tests +{ + public class MyFeatureUnitTests + { + private readonly IFixture _fixture = new Fixture(); + private ContentstackClient _client; + + public MyFeatureUnitTests() + { + var options = new ContentstackOptions() + { + ApiKey = _fixture.Create(), + DeliveryToken = _fixture.Create(), + Environment = _fixture.Create() + }; + _client = new ContentstackClient(new OptionsWrapper(options)); + } + + private Query CreateQuery(string contentTypeId = "source") + => _client.ContentType(contentTypeId).Query(); + + // Helper: get private QueryValueJson + private Dictionary GetQueryValueJson(Query query) + { + var field = typeof(Query).GetField("QueryValueJson", + BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy); + return (Dictionary)field?.GetValue(query); + } + + // Helper: get private UrlQueries + private Dictionary GetUrlQueries(Query query) + { + var field = typeof(Query).GetField("UrlQueries", + BindingFlags.NonPublic | BindingFlags.Instance); + return (Dictionary)field?.GetValue(query); + } + + [Fact] + public void MyOperator_AddsCorrectQueryParameter() + { + var query = CreateQuery(); + var key = _fixture.Create(); + + var result = query.MyOperator(key, "value"); + + Assert.Equal(query, result); // fluent return + var qvj = GetQueryValueJson(query); + Assert.True(qvj.ContainsKey(key)); + var inner = qvj[key] as Dictionary; + Assert.True(inner.ContainsKey("$myop")); + Assert.Equal("value", inner["$myop"]); + } + + [Fact] + public void MyUrlParam_AddsToUrlQueries() + { + var query = CreateQuery(); + + query.SetLocale("en-us"); + + var urlQueries = GetUrlQueries(query); + Assert.Equal("en-us", urlQueries["locale"]); + } + + [Fact] + public void MyOperator_WithNullKey_ThrowsQueryFilterException() + { + var query = CreateQuery(); + Assert.Throws(() => query.MyOperator(null, "value")); + } + + [Fact] + public void MyOperator_WithEmptyKey_ThrowsQueryFilterException() + { + var query = CreateQuery(); + Assert.Throws(() => query.MyOperator(string.Empty, "value")); + } + + [Fact] + public void MyOperator_ReturnsQueryForChaining() + { + var query = CreateQuery(); + var result = query + .MyOperator("field1", "value1") + .MyOperator("field2", "value2"); + Assert.Equal(query, result); + } + } +} +``` + +## Complete Integration Test Template + +```csharp +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; +using Contentstack.Core.Models; +using Contentstack.Core.Tests.Helpers; + +namespace Contentstack.Core.Tests.Integration.MyFeatureTests +{ + public class MyFeatureComprehensiveTest : IntegrationTestBase + { + public MyFeatureComprehensiveTest(ITestOutputHelper output) : base(output) + { + } + + [Fact(DisplayName = "MyFeature - BasicOperation ReturnsExpectedResult")] + public async Task MyFeature_BasicOperation_ReturnsExpectedResult() + { + // Arrange + LogArrange("Setting up basic operation test"); + LogContext("ContentType", TestDataHelper.SimpleContentTypeUid); + + var client = CreateClient(); + var query = client.ContentType(TestDataHelper.SimpleContentTypeUid).Query(); + + // Act + LogAct("Executing query with my feature"); + query.MyOperator("uid", "someValue"); + var result = await query.Find(); + + // Assert + LogAssert("Verifying response structure"); + TestAssert.NotNull(result); + TestAssert.NotNull(result.Items); + TestAssert.True(result.Count >= 0, "Count should be non-negative"); + } + + [Fact(DisplayName = "MyFeature - WithInvalidParams ThrowsException")] + public async Task MyFeature_WithInvalidParams_ThrowsException() + { + // Arrange + LogArrange("Setting up error scenario"); + var client = CreateClient(); + + // Act & Assert + LogAct("Executing with invalid parameters"); + await Assert.ThrowsAsync(async () => + { + await client.ContentType("nonexistent_type_12345") + .Query().Find(); + }); + } + } +} +``` + +## Existing Unit Test Files — What Each Covers + +| File | Covers | +|------|--------| +| `QueryUnitTests.cs` | All `Query` filter/operator methods, UrlQueries params | +| `EntryUnitTests.cs` | `Entry` field access, URL construction, header setting | +| `AssetUnitTests.cs` | `Asset` model fields | +| `AssetLibraryUnitTests.cs` | `AssetLibrary` query params, Skip/Limit | +| `ContentstackClientUnitTests.cs` | Client initialization, header injection, factory methods | +| `ContentstackOptionsUnitTests.cs` | Options defaults, validation | +| `ContentstackExceptionUnitTests.cs` | Exception hierarchy, factory methods, message content | +| `ConfigUnitTests.cs` | BaseUrl composition, region codes | +| `ContentstackRegionUnitTests.cs` | Region enum → URL prefix mapping | +| `GlobalFieldUnitTests.cs` | GlobalField ID validation, URL construction | +| `GlobalFieldQueryUnitTests.cs` | GlobalFieldQuery filter methods | +| `TaxonomyUnitTests.cs` | Taxonomy query path | +| `JsonConverterUnitTests.cs` | CSJsonConverter attribute registration | +| `LivePreviewConfigUnitTests.cs` | LivePreviewConfig validation | + +## Existing Integration Test Folders — What Each Covers + +| Folder | Covers | +|--------|--------| +| `QueryTests/` | Query operators, complex filters, field queries, includes | +| `EntryTests/` | Entry fetch, field projection, references | +| `GlobalFieldsTests/` | Global field schemas, nested global fields | +| `SyncTests/` | Sync API, pagination tokens, delta sync | +| `AssetTests/` | Asset fetch, asset library queries | +| `ContentTypeTests/` | Content type fetch, schema queries | +| `LocalizationTests/` | Locale filtering, locale fallback chains | +| `PaginationTests/` | Skip/Limit behavior, count accuracy | +| `ErrorHandling/` | API error codes, exception types, invalid params | +| `LivePreview/` | Live preview URL routing, token headers | +| `ModularBlocksTests/` | Modular block field deserialization | +| `MetadataTests/` | Entry metadata fields | +| `TaxonomyTests/` | Taxonomy query path, taxonomy filtering | +| `VariantsTests/` | Entry variant headers, variant content | +| `BranchTests/` | Branch header injection | + +## MockHttpHandler Pattern (Unit Tests) + +When you need to mock HTTP responses without network: + +```csharp +// In Mokes/MockHttpHandler.cs — extend for new mock scenarios +// MockResponse.cs — add JSON fixture strings for new response shapes +// MockInfrastructureTest.cs — base class wiring MockHttpHandler into client +``` + +## TestAssert Wrappers + +Use `TestAssert.*` instead of raw `Assert.*` in integration tests — they log assertion context to `ITestOutputHelper`: + +```csharp +TestAssert.NotNull(result); +TestAssert.Equal(expected, actual); +TestAssert.True(condition, "failure message"); +TestAssert.False(condition, "failure message"); +TestAssert.IsAssignableFrom>(result.Items); +TestAssert.Matches("^blt[a-zA-Z0-9]+$", entry.Uid); +``` + +## app.config Keys for Integration Tests + +Integration tests read config from `Contentstack.Core.Tests/app.config`: + +```xml + + + + + + + + + + +``` + +Never commit real credentials. Use environment variables or a secrets manager in CI. + +## Running Tests + +```bash +# Unit tests only (no credentials needed) +dotnet test Contentstack.Core.Unit.Tests/ + +# Integration tests (requires app.config with valid credentials) +dotnet test Contentstack.Core.Tests/ + +# Run specific category +dotnet test --filter "Category=RetryIntegration" + +# Run specific test class +dotnet test --filter "FullyQualifiedName~QueryOperatorsComprehensiveTest" +``` + +## Mokes folder (unit tests) + +`Contentstack.Core.Unit.Tests/Mokes/`: + +- `MockHttpHandler.cs` — intercepts HTTP without network +- `MockResponse.cs` — sample JSON response fixtures +- `MockInfrastructureTest.cs` — base for tests needing mock HTTP +- `Utilities.cs` — test utility helpers + +## RequestLoggingPlugin (integration) + +`CreateClient()` on `IntegrationTestBase` adds `RequestLoggingPlugin`, which logs HTTP requests and responses via `ITestOutputHelper`. No extra setup required. + +Custom plugins for a test: + +```csharp +var client = CreateClient(); +client.Plugins.Add(new MyTestPlugin()); +``` + +## Test coverage guidelines + +- Unit test: every new public `Query` method (operator or URL param) +- Unit test: null/invalid input → expected exception type +- Integration test: happy path with real API response +- Integration test: verify response shape (`Items`, `Count`, fields) +- Place integration tests under the folder that matches the feature area + +## Integration test file conventions + +- Folders mirror features: `Integration/QueryTests/`, `Integration/EntryTests/`, `Integration/GlobalFieldsTests/`, etc. +- One test class per broad concern when it makes sense; file names often end in `Test.cs` / `Tests.cs` +- Use `LogArrange` / `LogAct` / `LogAssert` / `LogContext` from `IntegrationTestBase` (see templates above) + +### xUnit traits (examples) + +```csharp +[Trait("Category", "RetryIntegration")] +[Trait("Category", "LivePreview")] +[Trait("Category", "Sync")] +``` + +### DisplayName convention + +```csharp +[Fact(DisplayName = "Query Operations - Regex Complex Pattern Matches Correctly")] +// FeatureArea - ComponentAction Outcome +``` From 724d8faa5386b2228479c49aadb2d28ce21d4591 Mon Sep 17 00:00:00 2001 From: raj pandey Date: Wed, 8 Apr 2026 12:14:37 +0530 Subject: [PATCH 2/2] Remvoed the refernces --- AGENTS.md | 4 +- skills/README.md | 2 +- skills/code-review/SKILL.md | 209 +++++++++++- .../references/code-review-checklist.md | 186 ----------- skills/code-review/references/red-flags.md | 15 - skills/dev-workflow/SKILL.md | 70 +++- .../dev-workflow/references/repo-tooling.md | 67 ---- skills/error-handling/SKILL.md | 275 +++++++++++++++- .../references/error-patterns.md | 274 ---------------- skills/models-and-serialization/SKILL.md | 231 ++++++++++++- .../references/serialization-patterns.md | 228 ------------- skills/query-building/SKILL.md | 281 +++++++++++++++- .../references/query-patterns.md | 280 ---------------- skills/sdk-core-patterns/SKILL.md | 199 ++++++++++- .../references/aspnetcore-integration.md | 48 --- .../references/sdk-architecture.md | 143 -------- skills/testing/SKILL.md | 310 +++++++++++++++++- skills/testing/references/testing-patterns.md | 309 ----------------- 18 files changed, 1553 insertions(+), 1578 deletions(-) delete mode 100644 skills/code-review/references/code-review-checklist.md delete mode 100644 skills/code-review/references/red-flags.md delete mode 100644 skills/dev-workflow/references/repo-tooling.md delete mode 100644 skills/error-handling/references/error-patterns.md delete mode 100644 skills/models-and-serialization/references/serialization-patterns.md delete mode 100644 skills/query-building/references/query-patterns.md delete mode 100644 skills/sdk-core-patterns/references/aspnetcore-integration.md delete mode 100644 skills/sdk-core-patterns/references/sdk-architecture.md delete mode 100644 skills/testing/references/testing-patterns.md diff --git a/AGENTS.md b/AGENTS.md index df8ef7bc..d8381ca1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # Contentstack .NET SDK – Agent guide -**Universal entry point** for contributors and AI agents. Each skill has a short **`skills/*/SKILL.md`** entry; exhaustive patterns, checklists, and examples live in **`skills/*/references/*.md`** where present. +**Universal entry point** for contributors and AI agents. Each skill is documented in **`skills/*/SKILL.md`** (YAML frontmatter for agent discovery where applicable). ## What this repo is @@ -46,4 +46,4 @@ An index with “when to use” hints is in [`skills/README.md`](skills/README.m ## Using Cursor (optional) -If you use **Cursor**, [`.cursor/rules/README.md`](.cursor/rules/README.md) only points to **`AGENTS.md`**—the same conventions as for everyone else. Canonical guidance remains in **`skills/*/SKILL.md`** and **`skills/*/references/`**. +If you use **Cursor**, [`.cursor/rules/README.md`](.cursor/rules/README.md) only points to **`AGENTS.md`**—the same conventions as for everyone else. Canonical guidance remains in **`skills/*/SKILL.md`**. diff --git a/skills/README.md b/skills/README.md index aafc465d..f3e91cdd 100644 --- a/skills/README.md +++ b/skills/README.md @@ -14,7 +14,7 @@ Source of truth for detailed guidance. Read **[AGENTS.md](../AGENTS.md)** first, | `testing` | Writing or debugging unit/integration tests, coverage, test layout. | | `code-review` | Reviewing a PR against SDK-specific checklist. | -Each folder contains **`SKILL.md`** with YAML frontmatter (`name`, `description`) for agent discovery. Long-form content is under **`references/`** when applicable—open `SKILL.md` first, then follow links. +Each folder contains **`SKILL.md`** with YAML frontmatter (`name`, `description`) for agent discovery. ### Cursor diff --git a/skills/code-review/SKILL.md b/skills/code-review/SKILL.md index f62068c0..9ea79041 100644 --- a/skills/code-review/SKILL.md +++ b/skills/code-review/SKILL.md @@ -16,11 +16,208 @@ description: SDK-specific PR review checklist for the Contentstack .NET SDK — - **Important** — Should fix (maintainability, SDK patterns, consistency) - **Suggestion** — Consider improving (style, optimization) -Category-by-category checklists (breaking changes, HTTP, exceptions, serialization, fluent API, config, tests, multi-targeting, plugins, visibility) live in the reference docs below—not duplicated here. +The sections below provide category-by-category checklists (breaking changes, HTTP, exceptions, serialization, fluent API, config, tests, multi-targeting, plugins, visibility) and a short red-flag list for quick scanning. -## Reference -| Document | Contents | -|----------|----------| -| [references/code-review-checklist.md](references/code-review-checklist.md) | Full markdown checklists per area | -| [references/red-flags.md](references/red-flags.md) | Short anti-pattern list for quick scan | +## Code Review Checklist + +### Breaking Changes Checklist + +```markdown +## Breaking Changes Review +- [ ] No public method signatures removed or changed without [Obsolete] deprecation +- [ ] No [JsonProperty] values changed (would break consumer deserialization silently) +- [ ] No ContentstackOptions public property removed +- [ ] New required options have defaults (don't break existing consumers who don't set them) +- [ ] No namespace renames without backward-compatible type aliases +- [ ] No IContentstackPlugin interface signature changed +- [ ] Version bump planned if breaking change is intentional (Directory.Build.props) +``` + +### HTTP Layer Checklist + +```markdown +## HTTP Layer Review +- [ ] All HTTP calls route through HttpRequestHandler.ProcessRequest +- [ ] No HttpClient instantiation anywhere in the PR +- [ ] New query params added to UrlQueries dict (not directly to URL string) +- [ ] New field-level filters added to QueryValueJson dict +- [ ] New headers added via Headers parameter to ProcessRequest +- [ ] Branch header uses "branch" key, passed as separate Branch parameter +- [ ] No hardcoded URLs — BaseUrl comes from Config.BaseUrl +- [ ] Live preview URL resolved via Config.getBaseUrl() — not hardcoded +- [ ] ProcessRequest result (string JSON) parsed, not further HTTP calls made +``` + +### Exception Handling Checklist + +```markdown +## Exception Handling Review +- [ ] Domain-specific exception type used (QueryFilterException, AssetException, etc.) +- [ ] No bare `throw new Exception(...)` or `throw new ContentstackException(...)` +- [ ] All message strings sourced from ErrorMessages.cs constants +- [ ] No string literals in throw statements +- [ ] GetContentstackError(ex) called when catching WebException from HTTP calls +- [ ] ErrorCode, StatusCode, Errors preserved when re-wrapping exceptions +- [ ] New domain area has new exception class with factory methods +- [ ] New error messages added to correct section in ErrorMessages.cs +- [ ] FormatExceptionDetails(innerEx) used in ProcessingError factory methods +``` + +### Serialization Checklist + +```markdown +## Serialization Review +- [ ] All public properties mapping CDA JSON fields have [JsonProperty("snake_case")] +- [ ] No reliance on default Newtonsoft.Json camelCase or PascalCase matching +- [ ] Custom deserialization uses [CSJsonConverter] + JsonConverter subclass +- [ ] JsonConverter placed in Contentstack.Core/Internals/ (internal class) +- [ ] No System.Text.Json usage +- [ ] No JsonConvert.DeserializeObject with hardcoded type outside of converter +- [ ] ContentstackCollection used for list responses (not List directly) +- [ ] "entries" token used for entry collection, "assets" for asset collection +``` + +### Fluent API Checklist + +```markdown +## Fluent API Review +- [ ] Every Query filter/operator method returns `return this;` +- [ ] Null key validated at start of method → QueryFilterException.Create() +- [ ] Empty string key validated → QueryFilterException.Create() +- [ ] Operator value stored in QueryValueJson[key][$operator] nested dict +- [ ] URL-level params stored in UrlQueries[key] +- [ ] Method name follows verb+noun pattern (GreaterThan, ContainedIn, NotExists) +- [ ] No mutation of QueryValueJson or UrlQueries outside of the Query class itself +- [ ] And()/Or() accept Query[] (not raw dictionaries) +``` + +### Configuration Checklist + +```markdown +## Configuration Review +- [ ] New options added to ContentstackOptions (public class), not Config (internal) +- [ ] New property has XML doc comment +- [ ] Default value set in ContentstackOptions() constructor or property initializer +- [ ] ContentstackClient constructor reads new option and passes to Config +- [ ] Config never exposed as public property +- [ ] New option tested in ContentstackOptionsUnitTests.cs +- [ ] ASP.NET Core binding works (IOptions path verified) +``` + +### Test Coverage Checklist + +```markdown +## Test Coverage Review +- [ ] Unit test for each new public Query method (QueryValueJson assertion via reflection) +- [ ] Unit test for null key input → QueryFilterException +- [ ] Unit test for empty key input → QueryFilterException +- [ ] Unit test for fluent return (Assert.Equal(query, result)) +- [ ] Integration test file in Integration/{FeatureName}Tests/ subfolder +- [ ] Integration test class extends IntegrationTestBase +- [ ] Integration test constructor takes ITestOutputHelper output +- [ ] CreateClient() used (not manual ContentstackClient construction) +- [ ] LogArrange/LogAct/LogAssert used in correct order +- [ ] TestAssert.* used (not raw Assert.*) +- [ ] [Fact(DisplayName = "FeatureArea - Component Description")] present +- [ ] Happy path test (valid params → expected response) +- [ ] Error path test (invalid params or not found → expected exception) +``` + +### Multi-Targeting Checklist + +```markdown +## Multi-Targeting Review +- [ ] No HttpClient (netstandard2.0 HttpClient has behavioural differences from net4x) +- [ ] No System.Text.Json (not available without separate package in netstandard2.0) +- [ ] No record types (C# 9, requires LangVersion setting for net47/net472) +- [ ] No default interface implementations (C# 8, may affect net47) +- [ ] No nullable reference types without #nullable enable guard +- [ ] No top-level statements (not applicable to library projects but worth checking) +- [ ] Tested compile against netstandard2.0 target (or verified via CI) +``` + +### Plugin Lifecycle Checklist + +```markdown +## Plugin Lifecycle Review +- [ ] New feature that makes HTTP calls uses HttpRequestHandler (plugins run automatically) +- [ ] No WebRequest.Create() called directly in new model classes +- [ ] IContentstackPlugin interface not modified (breaking for all plugin consumers) +- [ ] RequestLoggingPlugin still works with any new request/response changes +- [ ] Plugin.OnRequest receives HttpWebRequest before send +- [ ] Plugin.OnResponse receives response string (can mutate/inspect) +``` + +### Internal Visibility Checklist + +```markdown +## Internal Visibility Review +- [ ] New utility/helper classes in Internals/ are marked `internal` +- [ ] New model types intended for consumers are in Models/ and `public` +- [ ] New configuration types are in Configuration/ and `public` +- [ ] No public exposure of Config, HttpRequestHandler, or VersionUtility +- [ ] InternalsVisibleTo not modified (already covers both test projects) +- [ ] New internal methods accessible in unit tests without changes +``` + +### Common Issues Found in Past PRs + +#### Silent Deserialization Failures +`[JsonProperty]` omitted → field is always null at runtime, no exception. Verify all properties that map CDA JSON fields. + +#### Exception Message in Throw +```csharp +// Bad +throw new QueryFilterException("Please provide valid params."); + +// Good +throw QueryFilterException.Create(innerEx); +// or +throw new QueryFilterException(ErrorMessages.QueryFilterError); +``` + +#### Hardcoded Environment +```csharp +// Bad — breaks for consumers with different environments +mainJson["environment"] = "production"; + +// Correct — already done in Exec() +mainJson["environment"] = ContentTypeInstance.StackInstance.Config.Environment; +``` + +#### Returning void from Query Method +```csharp +// Bad — breaks fluent chaining +public void SetMyParam(string value) { UrlQueries["my_param"] = value; } + +// Good +public Query SetMyParam(string value) { UrlQueries["my_param"] = value; return this; } +``` + +#### Dictionary Not Initialized for QueryValueJson Entry +```csharp +// Bad — throws KeyNotFoundException or InvalidCastException +((Dictionary)QueryValueJson[key])["$op"] = value; + +// Good — guard with ContainsKey +if (!QueryValueJson.ContainsKey(key)) + QueryValueJson[key] = new Dictionary(); +((Dictionary)QueryValueJson[key])["$op"] = value; +``` + +## SDK-specific red flags + +Quick scan for anti-patterns in PRs: + +``` +❌ new HttpClient() — use HttpRequestHandler +❌ throw new Exception("message") — use typed ContentstackException subclass +❌ "hardcoded_field_name" — use [JsonProperty] or ErrorMessages constant +❌ public Config GetConfig() — Config is internal by design +❌ return void — Query methods return Query (fluent) +❌ [JsonProperty] omitted — CDA uses snake_case; PascalCase won't deserialize +❌ in .csproj — use Directory.Build.props +``` + +For full category-by-category review, see **Code review checklist** above. diff --git a/skills/code-review/references/code-review-checklist.md b/skills/code-review/references/code-review-checklist.md deleted file mode 100644 index 2e74306f..00000000 --- a/skills/code-review/references/code-review-checklist.md +++ /dev/null @@ -1,186 +0,0 @@ -# Code Review Checklist - -## Breaking Changes Checklist - -```markdown -## Breaking Changes Review -- [ ] No public method signatures removed or changed without [Obsolete] deprecation -- [ ] No [JsonProperty] values changed (would break consumer deserialization silently) -- [ ] No ContentstackOptions public property removed -- [ ] New required options have defaults (don't break existing consumers who don't set them) -- [ ] No namespace renames without backward-compatible type aliases -- [ ] No IContentstackPlugin interface signature changed -- [ ] Version bump planned if breaking change is intentional (Directory.Build.props) -``` - -## HTTP Layer Checklist - -```markdown -## HTTP Layer Review -- [ ] All HTTP calls route through HttpRequestHandler.ProcessRequest -- [ ] No HttpClient instantiation anywhere in the PR -- [ ] New query params added to UrlQueries dict (not directly to URL string) -- [ ] New field-level filters added to QueryValueJson dict -- [ ] New headers added via Headers parameter to ProcessRequest -- [ ] Branch header uses "branch" key, passed as separate Branch parameter -- [ ] No hardcoded URLs — BaseUrl comes from Config.BaseUrl -- [ ] Live preview URL resolved via Config.getBaseUrl() — not hardcoded -- [ ] ProcessRequest result (string JSON) parsed, not further HTTP calls made -``` - -## Exception Handling Checklist - -```markdown -## Exception Handling Review -- [ ] Domain-specific exception type used (QueryFilterException, AssetException, etc.) -- [ ] No bare `throw new Exception(...)` or `throw new ContentstackException(...)` -- [ ] All message strings sourced from ErrorMessages.cs constants -- [ ] No string literals in throw statements -- [ ] GetContentstackError(ex) called when catching WebException from HTTP calls -- [ ] ErrorCode, StatusCode, Errors preserved when re-wrapping exceptions -- [ ] New domain area has new exception class with factory methods -- [ ] New error messages added to correct section in ErrorMessages.cs -- [ ] FormatExceptionDetails(innerEx) used in ProcessingError factory methods -``` - -## Serialization Checklist - -```markdown -## Serialization Review -- [ ] All public properties mapping CDA JSON fields have [JsonProperty("snake_case")] -- [ ] No reliance on default Newtonsoft.Json camelCase or PascalCase matching -- [ ] Custom deserialization uses [CSJsonConverter] + JsonConverter subclass -- [ ] JsonConverter placed in Contentstack.Core/Internals/ (internal class) -- [ ] No System.Text.Json usage -- [ ] No JsonConvert.DeserializeObject with hardcoded type outside of converter -- [ ] ContentstackCollection used for list responses (not List directly) -- [ ] "entries" token used for entry collection, "assets" for asset collection -``` - -## Fluent API Checklist - -```markdown -## Fluent API Review -- [ ] Every Query filter/operator method returns `return this;` -- [ ] Null key validated at start of method → QueryFilterException.Create() -- [ ] Empty string key validated → QueryFilterException.Create() -- [ ] Operator value stored in QueryValueJson[key][$operator] nested dict -- [ ] URL-level params stored in UrlQueries[key] -- [ ] Method name follows verb+noun pattern (GreaterThan, ContainedIn, NotExists) -- [ ] No mutation of QueryValueJson or UrlQueries outside of the Query class itself -- [ ] And()/Or() accept Query[] (not raw dictionaries) -``` - -## Configuration Checklist - -```markdown -## Configuration Review -- [ ] New options added to ContentstackOptions (public class), not Config (internal) -- [ ] New property has XML doc comment -- [ ] Default value set in ContentstackOptions() constructor or property initializer -- [ ] ContentstackClient constructor reads new option and passes to Config -- [ ] Config never exposed as public property -- [ ] New option tested in ContentstackOptionsUnitTests.cs -- [ ] ASP.NET Core binding works (IOptions path verified) -``` - -## Test Coverage Checklist - -```markdown -## Test Coverage Review -- [ ] Unit test for each new public Query method (QueryValueJson assertion via reflection) -- [ ] Unit test for null key input → QueryFilterException -- [ ] Unit test for empty key input → QueryFilterException -- [ ] Unit test for fluent return (Assert.Equal(query, result)) -- [ ] Integration test file in Integration/{FeatureName}Tests/ subfolder -- [ ] Integration test class extends IntegrationTestBase -- [ ] Integration test constructor takes ITestOutputHelper output -- [ ] CreateClient() used (not manual ContentstackClient construction) -- [ ] LogArrange/LogAct/LogAssert used in correct order -- [ ] TestAssert.* used (not raw Assert.*) -- [ ] [Fact(DisplayName = "FeatureArea - Component Description")] present -- [ ] Happy path test (valid params → expected response) -- [ ] Error path test (invalid params or not found → expected exception) -``` - -## Multi-Targeting Checklist - -```markdown -## Multi-Targeting Review -- [ ] No HttpClient (netstandard2.0 HttpClient has behavioural differences from net4x) -- [ ] No System.Text.Json (not available without separate package in netstandard2.0) -- [ ] No record types (C# 9, requires LangVersion setting for net47/net472) -- [ ] No default interface implementations (C# 8, may affect net47) -- [ ] No nullable reference types without #nullable enable guard -- [ ] No top-level statements (not applicable to library projects but worth checking) -- [ ] Tested compile against netstandard2.0 target (or verified via CI) -``` - -## Plugin Lifecycle Checklist - -```markdown -## Plugin Lifecycle Review -- [ ] New feature that makes HTTP calls uses HttpRequestHandler (plugins run automatically) -- [ ] No WebRequest.Create() called directly in new model classes -- [ ] IContentstackPlugin interface not modified (breaking for all plugin consumers) -- [ ] RequestLoggingPlugin still works with any new request/response changes -- [ ] Plugin.OnRequest receives HttpWebRequest before send -- [ ] Plugin.OnResponse receives response string (can mutate/inspect) -``` - -## Internal Visibility Checklist - -```markdown -## Internal Visibility Review -- [ ] New utility/helper classes in Internals/ are marked `internal` -- [ ] New model types intended for consumers are in Models/ and `public` -- [ ] New configuration types are in Configuration/ and `public` -- [ ] No public exposure of Config, HttpRequestHandler, or VersionUtility -- [ ] InternalsVisibleTo not modified (already covers both test projects) -- [ ] New internal methods accessible in unit tests without changes -``` - -## Common Issues Found in Past PRs - -### Silent Deserialization Failures -`[JsonProperty]` omitted → field is always null at runtime, no exception. Verify all properties that map CDA JSON fields. - -### Exception Message in Throw -```csharp -// Bad -throw new QueryFilterException("Please provide valid params."); - -// Good -throw QueryFilterException.Create(innerEx); -// or -throw new QueryFilterException(ErrorMessages.QueryFilterError); -``` - -### Hardcoded Environment -```csharp -// Bad — breaks for consumers with different environments -mainJson["environment"] = "production"; - -// Correct — already done in Exec() -mainJson["environment"] = ContentTypeInstance.StackInstance.Config.Environment; -``` - -### Returning void from Query Method -```csharp -// Bad — breaks fluent chaining -public void SetMyParam(string value) { UrlQueries["my_param"] = value; } - -// Good -public Query SetMyParam(string value) { UrlQueries["my_param"] = value; return this; } -``` - -### Dictionary Not Initialized for QueryValueJson Entry -```csharp -// Bad — throws KeyNotFoundException or InvalidCastException -((Dictionary)QueryValueJson[key])["$op"] = value; - -// Good — guard with ContainsKey -if (!QueryValueJson.ContainsKey(key)) - QueryValueJson[key] = new Dictionary(); -((Dictionary)QueryValueJson[key])["$op"] = value; -``` diff --git a/skills/code-review/references/red-flags.md b/skills/code-review/references/red-flags.md deleted file mode 100644 index 3e5078d9..00000000 --- a/skills/code-review/references/red-flags.md +++ /dev/null @@ -1,15 +0,0 @@ -# SDK-specific red flags - -Quick scan for anti-patterns in PRs: - -``` -❌ new HttpClient() — use HttpRequestHandler -❌ throw new Exception("message") — use typed ContentstackException subclass -❌ "hardcoded_field_name" — use [JsonProperty] or ErrorMessages constant -❌ public Config GetConfig() — Config is internal by design -❌ return void — Query methods return Query (fluent) -❌ [JsonProperty] omitted — CDA uses snake_case; PascalCase won't deserialize -❌ in .csproj — use Directory.Build.props -``` - -For full category-by-category review, use [code-review-checklist.md](code-review-checklist.md). diff --git a/skills/dev-workflow/SKILL.md b/skills/dev-workflow/SKILL.md index e640c9ca..06f5ac70 100644 --- a/skills/dev-workflow/SKILL.md +++ b/skills/dev-workflow/SKILL.md @@ -16,11 +16,73 @@ description: Local development and CI workflow for the Contentstack .NET SDK — - Version in **`Directory.Build.props`** only for package version. - Integration tests need **`app.config`** (or equivalent) — do not commit secrets. -## Reference +## Repo tooling — CI, release, docs, security -| Document | Contents | -|----------|----------| -| [references/repo-tooling.md](references/repo-tooling.md) | Commands, CI, branch policy, CodeQL, NuGet release workflow, DocFX, SCA/policy scans, project layout | +### Local commands + +| Action | Command | +|--------|---------| +| Build | `dotnet build Contentstack.Net.sln` | +| Unit tests | `dotnet test Contentstack.Core.Unit.Tests/Contentstack.Core.Unit.Tests.csproj` | +| Integration tests | `dotnet test Contentstack.Core.Tests/Contentstack.Core.Tests.csproj` (credentials) | + +Package version: [`Directory.Build.props`](../../../Directory.Build.props) (``). + +### CI — unit tests + +[`.github/workflows/unit-test.yml`](../../../.github/workflows/unit-test.yml): on `pull_request` and `push`, Windows runner, .NET 7, `dotnet restore` → `dotnet build Contentstack.Net.sln` → `dotnet test` on `Contentstack.Core.Unit.Tests`. + +### CI — branch policy + +[`.github/workflows/check-branch.yml`](../../../.github/workflows/check-branch.yml): on `pull_request`, if base is `master` and head is not `staging`, the job fails with a message to open PRs from `staging` toward `master` per team policy. + +### CI — CodeQL + +[`.github/workflows/codeql-analysis.yml`](../../../.github/workflows/codeql-analysis.yml): static analysis on PRs (language matrix includes C# as configured in the workflow). + +### Release — NuGet + +[`.github/workflows/nuget-publish.yml`](../../../.github/workflows/nuget-publish.yml): triggered on **`release` `created`**. + +- `dotnet pack -c Release -o out` +- Push `contentstack.csharp.*.nupkg` to NuGet.org (`NUGET_API_KEY` / `NUGET_AUTH_TOKEN` secrets — names appear in workflow; values are org secrets). +- Secondary job may push to GitHub Packages (`nuget.pkg.github.com`). + +Maintainers: ensure `Directory.Build.props` version matches the release tag policy before publishing. + +### DocFX API docs + +Configuration: [`docfx_project/docfx.json`](../../../docfx_project/docfx.json). Output site: `_site/` under the docfx project. + +Prerequisite: [DocFX](https://dotnet.github.io/docfx/) CLI installed. + +```bash +cd docfx_project +docfx docfx.json +# or: docfx build docfx.json +``` + +If metadata `src` paths do not resolve (e.g. `src/**.csproj`), adjust `docfx.json` or project layout so metadata generation matches this repository’s structure. + +Related: [`filterRules.yml`](../../../docfx_project/filterRules.yml) for API filter rules. + +### Security / policy automation + +**SCA (dependencies):** [`.github/workflows/sca-scan.yml`](../../../.github/workflows/sca-scan.yml) — on PR, `dotnet restore`, then Snyk Dotnet action against `Contentstack.Core/obj/project.assets.json` (`SNYK_TOKEN` secret). Failures indicate vulnerable packages or scan misconfiguration. + +**Policy (repository hygiene):** [`.github/workflows/policy-scan.yml`](../../../.github/workflows/policy-scan.yml) — for public repos, checks presence of `SECURITY.md` (or `.github/SECURITY.md`) and a license file. Adjust repo contents if these jobs fail. + +When a PR fails these jobs, inspect the workflow log and fix dependencies or policy items as required by your team. + +### Projects (layout) + +| Path | Role | +|------|------| +| `Contentstack.Net.sln` | Main solution | +| `Contentstack.Core/` | Delivery SDK package | +| `Contentstack.AspNetCore/` | DI package | +| `Contentstack.Core.Unit.Tests/` | Unit tests | +| `Contentstack.Core.Tests/` | Integration tests | ## Related skills diff --git a/skills/dev-workflow/references/repo-tooling.md b/skills/dev-workflow/references/repo-tooling.md deleted file mode 100644 index 428a23fb..00000000 --- a/skills/dev-workflow/references/repo-tooling.md +++ /dev/null @@ -1,67 +0,0 @@ -# Repo tooling — CI, release, docs, security - -## Local commands - -| Action | Command | -|--------|---------| -| Build | `dotnet build Contentstack.Net.sln` | -| Unit tests | `dotnet test Contentstack.Core.Unit.Tests/Contentstack.Core.Unit.Tests.csproj` | -| Integration tests | `dotnet test Contentstack.Core.Tests/Contentstack.Core.Tests.csproj` (credentials) | - -Package version: [`Directory.Build.props`](../../../Directory.Build.props) (``). - -## CI — unit tests - -[`.github/workflows/unit-test.yml`](../../../.github/workflows/unit-test.yml): on `pull_request` and `push`, Windows runner, .NET 7, `dotnet restore` → `dotnet build Contentstack.Net.sln` → `dotnet test` on `Contentstack.Core.Unit.Tests`. - -## CI — branch policy - -[`.github/workflows/check-branch.yml`](../../../.github/workflows/check-branch.yml): on `pull_request`, if base is `master` and head is not `staging`, the job fails with a message to open PRs from `staging` toward `master` per team policy. - -## CI — CodeQL - -[`.github/workflows/codeql-analysis.yml`](../../../.github/workflows/codeql-analysis.yml): static analysis on PRs (language matrix includes C# as configured in the workflow). - -## Release — NuGet - -[`.github/workflows/nuget-publish.yml`](../../../.github/workflows/nuget-publish.yml): triggered on **`release` `created`**. - -- `dotnet pack -c Release -o out` -- Push `contentstack.csharp.*.nupkg` to NuGet.org (`NUGET_API_KEY` / `NUGET_AUTH_TOKEN` secrets — names appear in workflow; values are org secrets). -- Secondary job may push to GitHub Packages (`nuget.pkg.github.com`). - -Maintainers: ensure `Directory.Build.props` version matches the release tag policy before publishing. - -## DocFX API docs - -Configuration: [`docfx_project/docfx.json`](../../../docfx_project/docfx.json). Output site: `_site/` under the docfx project. - -Prerequisite: [DocFX](https://dotnet.github.io/docfx/) CLI installed. - -```bash -cd docfx_project -docfx docfx.json -# or: docfx build docfx.json -``` - -If metadata `src` paths do not resolve (e.g. `src/**.csproj`), adjust `docfx.json` or project layout so metadata generation matches this repository’s structure. - -Related: [`filterRules.yml`](../../../docfx_project/filterRules.yml) for API filter rules. - -## Security / policy automation - -**SCA (dependencies):** [`.github/workflows/sca-scan.yml`](../../../.github/workflows/sca-scan.yml) — on PR, `dotnet restore`, then Snyk Dotnet action against `Contentstack.Core/obj/project.assets.json` (`SNYK_TOKEN` secret). Failures indicate vulnerable packages or scan misconfiguration. - -**Policy (repository hygiene):** [`.github/workflows/policy-scan.yml`](../../../.github/workflows/policy-scan.yml) — for public repos, checks presence of `SECURITY.md` (or `.github/SECURITY.md`) and a license file. Adjust repo contents if these jobs fail. - -When a PR fails these jobs, inspect the workflow log and fix dependencies or policy items as required by your team. - -## Projects (layout) - -| Path | Role | -|------|------| -| `Contentstack.Net.sln` | Main solution | -| `Contentstack.Core/` | Delivery SDK package | -| `Contentstack.AspNetCore/` | DI package | -| `Contentstack.Core.Unit.Tests/` | Unit tests | -| `Contentstack.Core.Tests/` | Integration tests | diff --git a/skills/error-handling/SKILL.md b/skills/error-handling/SKILL.md index 375ff5b3..b4b08a52 100644 --- a/skills/error-handling/SKILL.md +++ b/skills/error-handling/SKILL.md @@ -22,6 +22,277 @@ description: Error handling patterns for the Contentstack .NET SDK — Contentst Base properties: `ErrorCode`, `StatusCode`, `Errors`. -## Reference +## Error Patterns Reference -Exhaustive detail: **[references/error-patterns.md](references/error-patterns.md)** — `ErrorMessages` catalogue, API error JSON shape, HTTP mapping, `GetContentstackError` locations, factories, catch patterns, Live Preview / global field triggers. +### Complete ErrorMessages Catalogue + +All strings in `Contentstack.Core/Internals/ErrorMessages.cs`: + +#### Query and Filter +```csharp +QueryFilterError = "Please provide valid params." +InvalidParamsError = "Invalid parameters provided. {0}" +``` + +#### Asset +```csharp +AssetJsonConversionError = "Failed to convert asset JSON. Please check the asset format and data integrity." +AssetProcessingError = "An error occurred while processing the asset. {0}" +AssetLibraryRequestError = "Exception in {0}: {1}\nStackTrace: {2}" +``` + +#### Entry +```csharp +EntryProcessingError = "An error occurred while processing the entry. {0}" +EntryUidRequired = "Please set entry uid." +EntryNotFoundInCache = "Entry is not present in cache" +``` + +#### Global Field +```csharp +GlobalFieldIdNullError = "GlobalFieldId required. This value cannot be null or empty, define it in your configuration." +GlobalFieldProcessingError = "An error occurred while processing the globalField. {0}" +GlobalFieldQueryError = "Global field query failed. Check your query syntax and field schema before retrying." +``` + +#### Live Preview +```csharp +LivePreviewTokenMissing = "Live Preview token missing. Add either a PreviewToken or a ManagementToken in the LivePreviewConfig." +``` + +#### Client Request +```csharp +ContentstackClientRequestError = "Contentstack client request failed. Check your network settings or request parameters and try again: {0}" +ContentstackSyncRequestError = "An error occurred while processing the Contentstack client request: {0}" +``` + +#### Taxonomy +```csharp +TaxonomyProcessingError = "An error occurred while processing the taxonomy operation: {0}" +``` + +#### Content Type +```csharp +ContentTypeProcessingError = "Content type processing failed. Verify the schema and ensure all required fields are configured." +``` + +#### Authentication and Configuration +```csharp +StackApiKeyRequired = "Stack api key can not be null." +AccessTokenRequired = "Access token can not be null." +EnvironmentRequired = "Environment can not be null." +AuthenticationNotPresent = "Authentication Not present." +ContentTypeNameRequired = "Please set contentType name." +``` + +#### JSON and Parsing +```csharp +InvalidJsonFormat = "Please provide valid JSON." +ParsingError = "Parsing Error." +``` + +#### Network and Server +```csharp +NoConnectionError = "Connection error" +ServerError = "Server interaction went wrong, Please try again." +NetworkUnavailable = "Network not available." +DefaultError = "Oops! Something went wrong. Please try again." +``` + +#### Cache +```csharp +SavingNetworkCallResponseForCache = "Error while saving network call response." +``` + +#### Initialization +```csharp +ContentstackDefaultMethodNotCalled = "You must called Contentstack.stack() first" +``` + +### CDA API Error Response Format + +The Contentstack CDA returns errors in this JSON format: + +```json +{ + "error_message": "The requested entry doesn't exist.", + "error_code": 141, + "errors": { + "field_name": ["validation message"] + } +} +``` + +`error_code` is Contentstack-specific (not HTTP status). Common codes: +- `141` — Entry not found +- `141` — Asset not found +- `109` — API key invalid +- `103` — Access token invalid +- `129` — Invalid query parameters + +### HTTP Status to Exception Mapping + +| HTTP Status | Typical cause | SDK behavior | +|------------|--------------|-------------| +| 400 | Invalid query params | `QueryFilterException` | +| 401 | Invalid API key / token | `ContentstackException` with StatusCode 401 | +| 404 | Entry / asset not found | `ContentstackException` with StatusCode 404 | +| 422 | Invalid field value | `ContentstackException` with `Errors` dict populated | +| 429 | Rate limit exceeded | `ContentstackException` with StatusCode 429 | +| 500 | Server error | `ContentstackException` with StatusCode 500 | + +### Where GetContentstackError Is Called + +The same static `GetContentstackError` method is replicated (acknowledged code smell) in: +- `Query` — wraps in `QueryFilterException` +- `Entry` — wraps in `EntryException` +- `ContentType` — wraps in `ContentTypeException` +- `Asset` — wraps in `AssetException` +- `AssetLibrary` — wraps in `AssetException` + +When adding a new model, follow the same pattern — copy `GetContentstackError` into the new class (or call the one from `Query` if same namespace/access level permits). + +### Exception with Errors Dictionary + +When the API returns field-level validation errors: + +```csharp +catch (ContentstackException ex) +{ + if (ex.Errors != null) + { + foreach (var field in ex.Errors) + { + // field.Key = field name, field.Value = error messages + Console.WriteLine($"Field '{field.Key}': {field.Value}"); + } + } +} +``` + +### LivePreviewException Trigger Conditions + +Thrown when `LivePreviewConfig.Enable = true` but neither `ManagementToken` nor `PreviewToken` is configured: + +```csharp +if (livePreviewConfig.Enable) +{ + if (string.IsNullOrEmpty(livePreviewConfig.ManagementToken) + && string.IsNullOrEmpty(livePreviewConfig.PreviewToken)) + throw new LivePreviewException(); // uses LivePreviewTokenMissing message +} +``` + +### GlobalFieldException.CreateForIdNull Trigger + +Thrown when `GlobalField(null)` or `GlobalField("")` is called — validated before any HTTP request: + +```csharp +public GlobalField GlobalField(string uid) +{ + if (string.IsNullOrEmpty(uid)) + throw GlobalFieldException.CreateForIdNull(); + // ... +} +``` + +### Adding Messages for New Features — Checklist + +1. Add `public const string` to the appropriate section in `ErrorMessages.cs` +2. Use `{0}` placeholder for `string.Format` when appending exception details +3. Add the factory method on the exception class using `ErrorMessages.FormatExceptionDetails(ex)` for processing errors +4. Never concatenate exception details manually — always use `FormatExceptionDetails()` + +--- + +### Static factory pattern (examples) + +Use factories — not `new XyzException("literal")`: + +```csharp +throw QueryFilterException.Create(innerException); +throw GlobalFieldException.CreateForIdNull(); +throw AssetException.CreateForJsonConversionError(); +throw AssetException.CreateForProcessingError(innerException); +throw EntryException.CreateForProcessingError(innerException); +throw TaxonomyException.CreateForProcessingError(innerException); +throw ContentTypeException.CreateForProcessingError(innerException); +``` + +### Adding a new domain exception + +1. Add the class in `ContentstackExceptions.cs` extending `ContentstackException` with static factories. +2. Add `public const string` entries in `ErrorMessages.cs`; use `{0}` when wrapping `FormatExceptionDetails(innerException)`. + +Example skeleton: + +```csharp +public class MyFeatureException : ContentstackException +{ + public MyFeatureException(string message) : base(message) { } + public MyFeatureException(string message, Exception innerException) : base(message, innerException) { } + + public static MyFeatureException CreateForProcessingError(Exception innerException) + { + return new MyFeatureException( + string.Format(ErrorMessages.MyFeatureProcessingError, + ErrorMessages.FormatExceptionDetails(innerException)), + innerException); + } +} +``` + +### GetContentstackError (implementation sketch) + +`WebException` responses are parsed into `ContentstackException` with `ErrorCode`, `StatusCode`, and `Errors`. The same helper pattern is duplicated on `Query`, `Entry`, `ContentType`, `Asset`, `AssetLibrary` — follow the existing copy when adding a new HTTP-calling model. + +```csharp +internal static ContentstackException GetContentstackError(Exception ex) +{ + var webEx = (WebException)ex; + using var stream = webEx.Response.GetResponseStream(); + string errorMessage = new StreamReader(stream).ReadToEnd(); + JObject data = JObject.Parse(errorMessage); + int errorCode = data["error_code"]?.Value() ?? 0; + HttpStatusCode statusCode = ((HttpWebResponse)webEx.Response).StatusCode; + var errors = data["errors"]?.ToObject>(); + return new ContentstackException(data["error_message"]?.Value()) + { + ErrorCode = errorCode, + StatusCode = statusCode, + Errors = errors + }; +} +``` + +### Standard catch block (SDK internals) + +Preserve `ErrorCode`, `StatusCode`, and `Errors` when re-throwing domain exceptions: + +```csharp +catch (Exception ex) +{ + ContentstackException error = GetContentstackError(ex); + throw new QueryFilterException(error.Message, ex) + { + ErrorCode = error.ErrorCode, + StatusCode = error.StatusCode, + Errors = error.Errors + }; +} +``` + +### Consumer catch order (example) + +```csharp +try { /* await query.Find() */ } +catch (QueryFilterException ex) { /* query validation */ } +catch (ContentstackException ex) { /* API / HTTP */ } +catch (Exception ex) { /* network / unknown */ } +``` + +### ErrorMessages.FormatExceptionDetails + +```csharp +ErrorMessages.FormatExceptionDetails(innerException) +``` diff --git a/skills/error-handling/references/error-patterns.md b/skills/error-handling/references/error-patterns.md deleted file mode 100644 index 23dbaf92..00000000 --- a/skills/error-handling/references/error-patterns.md +++ /dev/null @@ -1,274 +0,0 @@ -# Error Patterns Reference - -## Complete ErrorMessages Catalogue - -All strings in `Contentstack.Core/Internals/ErrorMessages.cs`: - -### Query and Filter -```csharp -QueryFilterError = "Please provide valid params." -InvalidParamsError = "Invalid parameters provided. {0}" -``` - -### Asset -```csharp -AssetJsonConversionError = "Failed to convert asset JSON. Please check the asset format and data integrity." -AssetProcessingError = "An error occurred while processing the asset. {0}" -AssetLibraryRequestError = "Exception in {0}: {1}\nStackTrace: {2}" -``` - -### Entry -```csharp -EntryProcessingError = "An error occurred while processing the entry. {0}" -EntryUidRequired = "Please set entry uid." -EntryNotFoundInCache = "Entry is not present in cache" -``` - -### Global Field -```csharp -GlobalFieldIdNullError = "GlobalFieldId required. This value cannot be null or empty, define it in your configuration." -GlobalFieldProcessingError = "An error occurred while processing the globalField. {0}" -GlobalFieldQueryError = "Global field query failed. Check your query syntax and field schema before retrying." -``` - -### Live Preview -```csharp -LivePreviewTokenMissing = "Live Preview token missing. Add either a PreviewToken or a ManagementToken in the LivePreviewConfig." -``` - -### Client Request -```csharp -ContentstackClientRequestError = "Contentstack client request failed. Check your network settings or request parameters and try again: {0}" -ContentstackSyncRequestError = "An error occurred while processing the Contentstack client request: {0}" -``` - -### Taxonomy -```csharp -TaxonomyProcessingError = "An error occurred while processing the taxonomy operation: {0}" -``` - -### Content Type -```csharp -ContentTypeProcessingError = "Content type processing failed. Verify the schema and ensure all required fields are configured." -``` - -### Authentication and Configuration -```csharp -StackApiKeyRequired = "Stack api key can not be null." -AccessTokenRequired = "Access token can not be null." -EnvironmentRequired = "Environment can not be null." -AuthenticationNotPresent = "Authentication Not present." -ContentTypeNameRequired = "Please set contentType name." -``` - -### JSON and Parsing -```csharp -InvalidJsonFormat = "Please provide valid JSON." -ParsingError = "Parsing Error." -``` - -### Network and Server -```csharp -NoConnectionError = "Connection error" -ServerError = "Server interaction went wrong, Please try again." -NetworkUnavailable = "Network not available." -DefaultError = "Oops! Something went wrong. Please try again." -``` - -### Cache -```csharp -SavingNetworkCallResponseForCache = "Error while saving network call response." -``` - -### Initialization -```csharp -ContentstackDefaultMethodNotCalled = "You must called Contentstack.stack() first" -``` - -## CDA API Error Response Format - -The Contentstack CDA returns errors in this JSON format: - -```json -{ - "error_message": "The requested entry doesn't exist.", - "error_code": 141, - "errors": { - "field_name": ["validation message"] - } -} -``` - -`error_code` is Contentstack-specific (not HTTP status). Common codes: -- `141` — Entry not found -- `141` — Asset not found -- `109` — API key invalid -- `103` — Access token invalid -- `129` — Invalid query parameters - -## HTTP Status to Exception Mapping - -| HTTP Status | Typical cause | SDK behavior | -|------------|--------------|-------------| -| 400 | Invalid query params | `QueryFilterException` | -| 401 | Invalid API key / token | `ContentstackException` with StatusCode 401 | -| 404 | Entry / asset not found | `ContentstackException` with StatusCode 404 | -| 422 | Invalid field value | `ContentstackException` with `Errors` dict populated | -| 429 | Rate limit exceeded | `ContentstackException` with StatusCode 429 | -| 500 | Server error | `ContentstackException` with StatusCode 500 | - -## Where GetContentstackError Is Called - -The same static `GetContentstackError` method is replicated (acknowledged code smell) in: -- `Query` — wraps in `QueryFilterException` -- `Entry` — wraps in `EntryException` -- `ContentType` — wraps in `ContentTypeException` -- `Asset` — wraps in `AssetException` -- `AssetLibrary` — wraps in `AssetException` - -When adding a new model, follow the same pattern — copy `GetContentstackError` into the new class (or call the one from `Query` if same namespace/access level permits). - -## Exception with Errors Dictionary - -When the API returns field-level validation errors: - -```csharp -catch (ContentstackException ex) -{ - if (ex.Errors != null) - { - foreach (var field in ex.Errors) - { - // field.Key = field name, field.Value = error messages - Console.WriteLine($"Field '{field.Key}': {field.Value}"); - } - } -} -``` - -## LivePreviewException Trigger Conditions - -Thrown when `LivePreviewConfig.Enable = true` but neither `ManagementToken` nor `PreviewToken` is configured: - -```csharp -if (livePreviewConfig.Enable) -{ - if (string.IsNullOrEmpty(livePreviewConfig.ManagementToken) - && string.IsNullOrEmpty(livePreviewConfig.PreviewToken)) - throw new LivePreviewException(); // uses LivePreviewTokenMissing message -} -``` - -## GlobalFieldException.CreateForIdNull Trigger - -Thrown when `GlobalField(null)` or `GlobalField("")` is called — validated before any HTTP request: - -```csharp -public GlobalField GlobalField(string uid) -{ - if (string.IsNullOrEmpty(uid)) - throw GlobalFieldException.CreateForIdNull(); - // ... -} -``` - -## Adding Messages for New Features — Checklist - -1. Add `public const string` to the appropriate section in `ErrorMessages.cs` -2. Use `{0}` placeholder for `string.Format` when appending exception details -3. Add the factory method on the exception class using `ErrorMessages.FormatExceptionDetails(ex)` for processing errors -4. Never concatenate exception details manually — always use `FormatExceptionDetails()` - ---- - -## Static factory pattern (examples) - -Use factories — not `new XyzException("literal")`: - -```csharp -throw QueryFilterException.Create(innerException); -throw GlobalFieldException.CreateForIdNull(); -throw AssetException.CreateForJsonConversionError(); -throw AssetException.CreateForProcessingError(innerException); -throw EntryException.CreateForProcessingError(innerException); -throw TaxonomyException.CreateForProcessingError(innerException); -throw ContentTypeException.CreateForProcessingError(innerException); -``` - -## Adding a new domain exception - -1. Add the class in `ContentstackExceptions.cs` extending `ContentstackException` with static factories. -2. Add `public const string` entries in `ErrorMessages.cs`; use `{0}` when wrapping `FormatExceptionDetails(innerException)`. - -Example skeleton: - -```csharp -public class MyFeatureException : ContentstackException -{ - public MyFeatureException(string message) : base(message) { } - public MyFeatureException(string message, Exception innerException) : base(message, innerException) { } - - public static MyFeatureException CreateForProcessingError(Exception innerException) - { - return new MyFeatureException( - string.Format(ErrorMessages.MyFeatureProcessingError, - ErrorMessages.FormatExceptionDetails(innerException)), - innerException); - } -} -``` - -## GetContentstackError (implementation sketch) - -`WebException` responses are parsed into `ContentstackException` with `ErrorCode`, `StatusCode`, and `Errors`. The same helper pattern is duplicated on `Query`, `Entry`, `ContentType`, `Asset`, `AssetLibrary` — follow the existing copy when adding a new HTTP-calling model. - -```csharp -internal static ContentstackException GetContentstackError(Exception ex) -{ - var webEx = (WebException)ex; - using var stream = webEx.Response.GetResponseStream(); - string errorMessage = new StreamReader(stream).ReadToEnd(); - JObject data = JObject.Parse(errorMessage); - int errorCode = data["error_code"]?.Value() ?? 0; - HttpStatusCode statusCode = ((HttpWebResponse)webEx.Response).StatusCode; - var errors = data["errors"]?.ToObject>(); - return new ContentstackException(data["error_message"]?.Value()) - { - ErrorCode = errorCode, - StatusCode = statusCode, - Errors = errors - }; -} -``` - -## Standard catch block (SDK internals) - -Preserve `ErrorCode`, `StatusCode`, and `Errors` when re-throwing domain exceptions: - -```csharp -catch (Exception ex) -{ - ContentstackException error = GetContentstackError(ex); - throw new QueryFilterException(error.Message, ex) - { - ErrorCode = error.ErrorCode, - StatusCode = error.StatusCode, - Errors = error.Errors - }; -} -``` - -## Consumer catch order (example) - -```csharp -try { /* await query.Find() */ } -catch (QueryFilterException ex) { /* query validation */ } -catch (ContentstackException ex) { /* API / HTTP */ } -catch (Exception ex) { /* network / unknown */ } -``` - -## ErrorMessages.FormatExceptionDetails - -```csharp -ErrorMessages.FormatExceptionDetails(innerException) -``` diff --git a/skills/models-and-serialization/SKILL.md b/skills/models-and-serialization/SKILL.md index 723f2f4e..c7a49100 100644 --- a/skills/models-and-serialization/SKILL.md +++ b/skills/models-and-serialization/SKILL.md @@ -15,9 +15,234 @@ description: Model and serialization patterns for the Contentstack .NET SDK — - CDA JSON is **snake_case** — use **`[JsonProperty("snake_case")]`** on mapped properties; do not rely on default PascalCase naming. - Custom converters: **`[CSJsonConverter("Name")]`** on the model + **`JsonConverter`** implementation in **`Contentstack.Core/Internals/`**. -- New converters are registered automatically at client init (see reference). +- New converters are registered automatically at client init (see **CSJsonConverter registration flow** below). - Use **`Newtonsoft.Json`** only — not `System.Text.Json`. -## Reference +## Serialization Patterns Reference -**[references/serialization-patterns.md](references/serialization-patterns.md)** — consumer patterns (Entry/Asset, Fetch/Find, JsonProperty, variants, new models, SerializerSettings), converter internals, `parseJObject`, RTE/modular blocks, collection edge cases. +### CSJsonConverter Registration Flow + +At `ContentstackClient` construction time: + +``` +1. Scan all assemblies in current AppDomain +2. Find all types with [CSJsonConverter("ConverterName")] attribute +3. Find the JsonConverter class with matching name in Contentstack.Core.Internals +4. Instantiate it and add to SerializerSettings.Converters +5. All subsequent Fetch/Find calls use these converters +``` + +This means **converter registration is automatic** — adding the attribute and converter class is all that's required. + +### EntryJsonConverter Internals + +`EntryJsonConverter` handles the nested entry JSON structure from the CDA: + +```json +{ + "uid": "blt123", + "title": "My Entry", + "publish_details": { "environment": "production", "locale": "en-us", "time": "...", "user": "..." }, + "locale": "en-us", + "_metadata": { ... }, + "custom_field": "value", + "reference_field": [{ "uid": "blt456", "_content_type_uid": "blog" }] +} +``` + +The converter: +1. Reads the raw `JObject` +2. Maps known fields to strongly-typed properties (`Uid`, `Title`, `PublishDetails`, etc.) +3. Puts all remaining fields into `entry.Object` (the catch-all dictionary) + +### AssetJsonConverter Internals + +Similar pattern for assets — maps `uid`, `title`, `url`, `content_type`, `file_size`, `filename`, `tags` to typed properties; remaining fields to `asset.Object`. + +### parseJObject\ in Query + +After `HttpRequestHandler.ProcessRequest` returns a JSON string, `Query.parseJObject` does: + +```csharp +JObject jObject = JObject.Parse(responseString); + +// For entry queries +JArray entries = (JArray)jObject["entries"]; +collection.Items = entries.ToObject>(client.Serializer); +collection.Count = jObject["count"]?.Value() ?? 0; +collection.Skip = (int)UrlQueries.GetValueOrDefault("skip", 0); +collection.Limit = (int)UrlQueries.GetValueOrDefault("limit", 100); +``` + +The `"entries"` token name is fixed by the CDA response format. Asset library uses `"assets"`. + +### JsonProperty Mapping Reference + +Key mappings already in the codebase: + +| JSON field | C# property | Model | +|-----------|-------------|-------| +| `publish_details` | `PublishDetails` | `Entry` | +| `_metadata` | `Metadata` / `_metadata` | `Entry` | +| `content_type` | `ContentType` | `Asset` | +| `file_size` | `FileSize` | `Asset` | +| `filename` | `FileName` | `Asset` | +| `created_at` | `CreatedAt` | various | +| `updated_at` | `UpdatedAt` | various | +| `created_by` | `CreatedBy` | various | + +### Newtonsoft.Json Settings Defaults + +The SDK uses default `JsonSerializerSettings` with no special configuration unless consumers override `client.SerializerSettings`. This means: +- `NullValueHandling.Include` (nulls are included) +- No date format override (ISO 8601 by default) +- No contract resolver override (property names as-is, so `[JsonProperty]` is required) +- No type name handling + +### Handling Embedded RTE Items + +RTE (Rich Text Editor) fields with embedded entries/assets are processed via `contentstack.utils` NuGet package. The `Entry` model surfaces the raw RTE JSON; consumers call the utils library to resolve embedded references: + +```csharp +// RTE field value from entry.Object["rte_field"] +// Pass to contentstack.utils for resolution +Utils.RenderContent(content, entryEmbeds); +``` + +### Deep Reference Deserialization + +When `IncludeReference()` is called, the CDA returns nested objects inside the entry JSON. These are deserialized as nested dictionaries in `entry.Object` or as typed sub-objects when the consumer POCO uses `[JsonProperty]` on the reference field: + +```csharp +public class BlogPost +{ + [JsonProperty("author")] + public Author Author { get; set; } // auto-deserialized if CDA returns expanded ref +} +``` + +If the reference is not expanded (not included), it will be an array of `{"uid": "...", "_content_type_uid": "..."}` objects. + +### Modular Blocks Deserialization + +Modular blocks are returned as JSON arrays of objects, each with a `_content_type_uid` discriminator: + +```json +"modular_blocks": [ + { "block_a": { "field1": "value" } }, + { "block_b": { "field2": "value" } } +] +``` + +Map with a `List>` or use a custom converter with a discriminator switch on the first key. + +### ContentstackCollection Parsing Edge Cases + +- `count` field only present when `IncludeCount()` is set on the query +- `entries` array is present even when empty (`[]`), never `null` +- `skip` and `limit` in the response may differ from what was requested if the CDA has its own limits + +--- + +### Entry model shape (quick reference) + +```csharp +// Strongly-typed fields (typical) +entry.Uid; entry.Title; entry.Tags; entry.Metadata; entry.PublishDetails; + +// Catch-all for arbitrary content type fields +entry.Object // Dictionary +``` + +```csharp +var price = entry.Object["price"]; +var color = entry.Object["color"] as string; +``` + +### Fetch and Find with typed POCOs + +Prefer typed models over `entry.Object` for structured access: + +```csharp +public class Product +{ + [JsonProperty("title")] + public string Title { get; set; } + + [JsonProperty("price")] + public decimal Price { get; set; } + + [JsonProperty("uid")] + public string Uid { get; set; } +} + +var result = await client.ContentType("product").Query().Find(); +var product = await client.ContentType("product").Entry("uid").Fetch(); +``` + +Deserialization uses `client.Serializer` (from `client.SerializerSettings`). + +### JsonProperty — always map snake_case + +The CDA uses `snake_case`. Newtonsoft defaults to PascalCase property names. Always annotate: + +```csharp +[JsonProperty("publish_details")] +public object PublishDetails { get; set; } +``` + +Without `[JsonProperty]`, deserialization looks for the wrong JSON keys. + +### CSJsonConverter on models + +```csharp +[CSJsonConverter("EntryJsonConverter")] +public class Entry { ... } +``` + +Converters live in `Contentstack.Core/Internals/`. See [CSJsonConverter Registration Flow](#csjsonconverter-registration-flow). + +### ContentstackCollection shape + +```csharp +public class ContentstackCollection +{ + public IEnumerable Items { get; } + public int Count { get; } + public int Skip { get; } + public int Limit { get; } +} +``` + +Parsed from `"entries"` or `"assets"` in `Query.parseJObject` (see [parseJObject\ in Query](#parsejobjectt-in-query)). + +### Asset model (common properties) + +`Uid`, `Title`, `Url`, `ContentType`, `FileSize`, `FileName`, `Tags`, plus `Object` for other fields. `AssetJsonConverter` handles nested JSON; `AssetLibrary.FetchAll()` returns `ContentstackCollection`. + +### Entry variants + +```csharp +entry.SetVariant("variant_uid"); +var result = await entry.Fetch(); +``` + +Uses `_variant` and the `x-cs-variant-uid` header path as implemented on `Entry`. + +### Adding a new model type (checklist) + +1. Add class under `Contentstack.Core/Models/` with `[JsonProperty]` on API fields and optional `Object` catch-all. +2. If custom deserialization is required, add `internal class MyModelJsonConverter : JsonConverter` in `Internals/`, implement `ReadJson` / `WriteJson` as needed (delivery is typically read-heavy). +3. Mark the model with `[CSJsonConverter("MyModelJsonConverter")]`. +4. Expose via `ContentstackClient` factory methods if it is a first-class API surface. + +### SerializerSettings customization + +Consumers may adjust before calls: + +```csharp +client.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; +client.SerializerSettings.DateFormatString = "yyyy-MM-dd"; +``` + +`Serializer` is rebuilt from `SerializerSettings` when needed. diff --git a/skills/models-and-serialization/references/serialization-patterns.md b/skills/models-and-serialization/references/serialization-patterns.md deleted file mode 100644 index dc80c340..00000000 --- a/skills/models-and-serialization/references/serialization-patterns.md +++ /dev/null @@ -1,228 +0,0 @@ -# Serialization Patterns Reference - -## CSJsonConverter Registration Flow - -At `ContentstackClient` construction time: - -``` -1. Scan all assemblies in current AppDomain -2. Find all types with [CSJsonConverter("ConverterName")] attribute -3. Find the JsonConverter class with matching name in Contentstack.Core.Internals -4. Instantiate it and add to SerializerSettings.Converters -5. All subsequent Fetch/Find calls use these converters -``` - -This means **converter registration is automatic** — adding the attribute and converter class is all that's required. - -## EntryJsonConverter Internals - -`EntryJsonConverter` handles the nested entry JSON structure from the CDA: - -```json -{ - "uid": "blt123", - "title": "My Entry", - "publish_details": { "environment": "production", "locale": "en-us", "time": "...", "user": "..." }, - "locale": "en-us", - "_metadata": { ... }, - "custom_field": "value", - "reference_field": [{ "uid": "blt456", "_content_type_uid": "blog" }] -} -``` - -The converter: -1. Reads the raw `JObject` -2. Maps known fields to strongly-typed properties (`Uid`, `Title`, `PublishDetails`, etc.) -3. Puts all remaining fields into `entry.Object` (the catch-all dictionary) - -## AssetJsonConverter Internals - -Similar pattern for assets — maps `uid`, `title`, `url`, `content_type`, `file_size`, `filename`, `tags` to typed properties; remaining fields to `asset.Object`. - -## parseJObject\ in Query - -After `HttpRequestHandler.ProcessRequest` returns a JSON string, `Query.parseJObject` does: - -```csharp -JObject jObject = JObject.Parse(responseString); - -// For entry queries -JArray entries = (JArray)jObject["entries"]; -collection.Items = entries.ToObject>(client.Serializer); -collection.Count = jObject["count"]?.Value() ?? 0; -collection.Skip = (int)UrlQueries.GetValueOrDefault("skip", 0); -collection.Limit = (int)UrlQueries.GetValueOrDefault("limit", 100); -``` - -The `"entries"` token name is fixed by the CDA response format. Asset library uses `"assets"`. - -## JsonProperty Mapping Reference - -Key mappings already in the codebase: - -| JSON field | C# property | Model | -|-----------|-------------|-------| -| `publish_details` | `PublishDetails` | `Entry` | -| `_metadata` | `Metadata` / `_metadata` | `Entry` | -| `content_type` | `ContentType` | `Asset` | -| `file_size` | `FileSize` | `Asset` | -| `filename` | `FileName` | `Asset` | -| `created_at` | `CreatedAt` | various | -| `updated_at` | `UpdatedAt` | various | -| `created_by` | `CreatedBy` | various | - -## Newtonsoft.Json Settings Defaults - -The SDK uses default `JsonSerializerSettings` with no special configuration unless consumers override `client.SerializerSettings`. This means: -- `NullValueHandling.Include` (nulls are included) -- No date format override (ISO 8601 by default) -- No contract resolver override (property names as-is, so `[JsonProperty]` is required) -- No type name handling - -## Handling Embedded RTE Items - -RTE (Rich Text Editor) fields with embedded entries/assets are processed via `contentstack.utils` NuGet package. The `Entry` model surfaces the raw RTE JSON; consumers call the utils library to resolve embedded references: - -```csharp -// RTE field value from entry.Object["rte_field"] -// Pass to contentstack.utils for resolution -Utils.RenderContent(content, entryEmbeds); -``` - -## Deep Reference Deserialization - -When `IncludeReference()` is called, the CDA returns nested objects inside the entry JSON. These are deserialized as nested dictionaries in `entry.Object` or as typed sub-objects when the consumer POCO uses `[JsonProperty]` on the reference field: - -```csharp -public class BlogPost -{ - [JsonProperty("author")] - public Author Author { get; set; } // auto-deserialized if CDA returns expanded ref -} -``` - -If the reference is not expanded (not included), it will be an array of `{"uid": "...", "_content_type_uid": "..."}` objects. - -## Modular Blocks Deserialization - -Modular blocks are returned as JSON arrays of objects, each with a `_content_type_uid` discriminator: - -```json -"modular_blocks": [ - { "block_a": { "field1": "value" } }, - { "block_b": { "field2": "value" } } -] -``` - -Map with a `List>` or use a custom converter with a discriminator switch on the first key. - -## ContentstackCollection Parsing Edge Cases - -- `count` field only present when `IncludeCount()` is set on the query -- `entries` array is present even when empty (`[]`), never `null` -- `skip` and `limit` in the response may differ from what was requested if the CDA has its own limits - ---- - -## Entry model shape (quick reference) - -```csharp -// Strongly-typed fields (typical) -entry.Uid; entry.Title; entry.Tags; entry.Metadata; entry.PublishDetails; - -// Catch-all for arbitrary content type fields -entry.Object // Dictionary -``` - -```csharp -var price = entry.Object["price"]; -var color = entry.Object["color"] as string; -``` - -## Fetch and Find with typed POCOs - -Prefer typed models over `entry.Object` for structured access: - -```csharp -public class Product -{ - [JsonProperty("title")] - public string Title { get; set; } - - [JsonProperty("price")] - public decimal Price { get; set; } - - [JsonProperty("uid")] - public string Uid { get; set; } -} - -var result = await client.ContentType("product").Query().Find(); -var product = await client.ContentType("product").Entry("uid").Fetch(); -``` - -Deserialization uses `client.Serializer` (from `client.SerializerSettings`). - -## JsonProperty — always map snake_case - -The CDA uses `snake_case`. Newtonsoft defaults to PascalCase property names. Always annotate: - -```csharp -[JsonProperty("publish_details")] -public object PublishDetails { get; set; } -``` - -Without `[JsonProperty]`, deserialization looks for the wrong JSON keys. - -## CSJsonConverter on models - -```csharp -[CSJsonConverter("EntryJsonConverter")] -public class Entry { ... } -``` - -Converters live in `Contentstack.Core/Internals/`. See [CSJsonConverter Registration Flow](#csjsonconverter-registration-flow). - -## ContentstackCollection shape - -```csharp -public class ContentstackCollection -{ - public IEnumerable Items { get; } - public int Count { get; } - public int Skip { get; } - public int Limit { get; } -} -``` - -Parsed from `"entries"` or `"assets"` in `Query.parseJObject` (see [parseJObject\ in Query](#parsejobjectt-in-query)). - -## Asset model (common properties) - -`Uid`, `Title`, `Url`, `ContentType`, `FileSize`, `FileName`, `Tags`, plus `Object` for other fields. `AssetJsonConverter` handles nested JSON; `AssetLibrary.FetchAll()` returns `ContentstackCollection`. - -## Entry variants - -```csharp -entry.SetVariant("variant_uid"); -var result = await entry.Fetch(); -``` - -Uses `_variant` and the `x-cs-variant-uid` header path as implemented on `Entry`. - -## Adding a new model type (checklist) - -1. Add class under `Contentstack.Core/Models/` with `[JsonProperty]` on API fields and optional `Object` catch-all. -2. If custom deserialization is required, add `internal class MyModelJsonConverter : JsonConverter` in `Internals/`, implement `ReadJson` / `WriteJson` as needed (delivery is typically read-heavy). -3. Mark the model with `[CSJsonConverter("MyModelJsonConverter")]`. -4. Expose via `ContentstackClient` factory methods if it is a first-class API surface. - -## SerializerSettings customization - -Consumers may adjust before calls: - -```csharp -client.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; -client.SerializerSettings.DateFormatString = "yyyy-MM-dd"; -``` - -`Serializer` is rebuilt from `SerializerSettings` when needed. diff --git a/skills/query-building/SKILL.md b/skills/query-building/SKILL.md index bed4e9a5..fa9bbd80 100644 --- a/skills/query-building/SKILL.md +++ b/skills/query-building/SKILL.md @@ -30,6 +30,283 @@ foreach (var kvp in UrlQueries) // → HttpRequestHandler.ProcessRequest(..., BodyJson) ``` -## Reference +## Query Patterns Reference -All exhaustive patterns, tables, and examples: **[references/query-patterns.md](references/query-patterns.md)** (quick-reference tables, extending `Query.cs`, terminal operations, pagination, sync, taxonomy URL paths, asset library, common mistakes). +### Quick reference: QueryValueJson operators + +| Method | Operator | Example JSON shape | +|--------|----------|-------------------| +| `Where(key, value)` | direct equality | `{"field": "value"}` | +| `NotEqualTo(key, value)` | `$ne` | `{"field":{"$ne":"x"}}` | +| `ContainedIn(key, values)` | `$in` | `{"field":{"$in":["a","b"]}}` | +| `NotContainedIn(key, values)` | `$nin` | `{"field":{"$nin":["a"]}}` | +| `Exists(key)` | `$exists: true` | `{"field":{"$exists":true}}` | +| `NotExists(key)` | `$exists: false` | `{"field":{"$exists":false}}` | +| `GreaterThan(key, value)` | `$gt` | `{"field":{"$gt":10}}` | +| `LessThan(key, value)` | `$lt` | `{"field":{"$lt":10}}` | +| `Regex(key, pattern)` | `$regex` | `{"field":{"$regex":"^blt"}}` | +| `And(queries[])` | `$and` | array of query objects | +| `Or(queries[])` | `$or` | array of query objects | + +### Quick reference: UrlQueries keys + +| Method | Key | Notes | +|--------|-----|-------| +| `SetLocale(locale)` | `locale` | prefer over obsolete `Language` enum | +| `Skip(n)` | `skip` | pagination offset | +| `Limit(n)` | `limit` | pagination page size | +| `IncludeSchema()` | `include_schema` | `true` | +| `IncludeCount()` | `include_count` | `true` | +| `Tags(tags[])` | `tags` | `string[]` → repeated param | +| `Only(fields[])` | `only[BASE][]` | field projection | +| `Except(fields[])` | `except[BASE][]` | field exclusion | +| `IncludeReference(key)` | `include[]` | reference expansion | + +### Extending Query.cs (new fluent methods) + +#### New Mongo-style filter operator + +```csharp +// Pattern used by all existing operators in Query.cs +public Query MyNewOperator(string key, object value) +{ + if (key == null) + throw QueryFilterException.Create(new ArgumentNullException(nameof(key))); + + if (!QueryValueJson.ContainsKey(key)) + QueryValueJson[key] = new Dictionary(); + + ((Dictionary)QueryValueJson[key])["$myop"] = value; + return this; // always return this for fluent chaining +} +``` + +#### New URL-level parameter + +```csharp +public Query MyParam(string value) +{ + UrlQueries["my_param"] = value; + return this; +} +``` + +### Terminal operations + +```csharp +Task> result = await query.Find(); +Task result = await query.FindOne(); +Task count = await query.Count(); +``` + +### Paginating Find results (entries) + +```csharp +// ContentstackCollection response shape +result.Items // IEnumerable +result.Count // total count (requires IncludeCount()) +result.Skip // current offset +result.Limit // current page size + +// Paging loop +int skip = 0, limit = 100; +ContentstackCollection page; +do { + page = await query.Skip(skip).Limit(limit).Find(); + // process page.Items + skip += limit; +} while (page.Items.Count() == limit); +``` + +### SyncStack short reference + +When using `SyncRecursive` / sync APIs: + +```csharp +SyncStack syncResult = await client.SyncRecursive(parameters); +// SyncStack.PaginationToken — non-null while more pages exist +// SyncStack.SyncToken — final token for next delta sync +``` + +(See [Sync API Patterns](#sync-api-patterns) below for full sync flows.) + +### Complete Operator Categories + +#### Comparison Operators + +```csharp +query.Where("title", "My Entry") // direct equality +query.NotEqualTo("status", "draft") // $ne +query.GreaterThan("price", 100) // $gt +query.GreaterThanEqualTo("price", 100) // $gte +query.LessThan("price", 200) // $lt +query.LessThanEqualTo("price", 200) // $lte +``` + +#### Array / Set Operators + +```csharp +query.ContainedIn("color", new[] {"red", "blue"}) // $in +query.NotContainedIn("color", new[] {"green"}) // $nin +``` + +#### Existence Operators + +```csharp +query.Exists("field_name") // $exists: true +query.NotExists("field_name") // $exists: false +``` + +#### String Operators + +```csharp +query.Regex("uid", "^blt[a-zA-Z0-9]+$") // $regex +query.Regex("title", "^hello", "i") // $regex with $options modifier +``` + +#### Logical Operators + +```csharp +// And / Or take Query[] — each sub-query builds its own QueryValueJson +var q1 = client.ContentType("ct").Query().Where("color", "red"); +var q2 = client.ContentType("ct").Query().Where("size", "large"); +query.And(new[] { q1, q2 }); // $and +query.Or(new[] { q1, q2 }); // $or +``` + +### Reference / Include Patterns + +```csharp +query.IncludeReference("reference_field"); // expand single reference +query.IncludeReference(new[] {"ref1", "ref2"}); // expand multiple +query.IncludeSchema(); // include content type schema +query.IncludeCount(); // include total count in response +query.IncludeOwner(); // include entry owner info +query.IncludeMetadata(); // include entry metadata +``` + +### Field Projection + +```csharp +query.Only(new[] {"title", "uid", "price"}); // return only these fields +query.Except(new[] {"body", "image"}); // exclude these fields + +// For referenced fields +query.OnlyWithReferenceUid(new[] {"title"}, "reference_field"); +query.ExceptWithReferenceUid(new[] {"body"}, "reference_field"); +``` + +### Ordering + +```csharp +query.OrderByAscending("title"); +query.OrderByDescending("created_at"); +``` + +### Locale and Environment + +```csharp +query.SetLocale("en-us"); // preferred — string locale code +// Environment is injected automatically from Config in Exec() +// Never set "environment" manually in UrlQueries +``` + +### Exec() Implementation Detail + +The full merge performed in `Query.Exec()` before calling `HttpRequestHandler`: + +```csharp +var mainJson = new Dictionary(); + +// 1. Environment (always injected from Config) +mainJson["environment"] = ContentTypeInstance.StackInstance.Config.Environment; + +// 2. Live preview headers (if enabled and content type matches) +if (livePreviewActive) +{ + mainJson["live_preview"] = livePreviewConfig.LivePreview; + mainJson["authorization"] = livePreviewConfig.ManagementToken; + // or mainJson["preview_token"] = livePreviewConfig.PreviewToken; +} + +// 3. Mongo-style query filter (only if non-empty) +if (QueryValueJson.Count > 0) + mainJson["query"] = QueryValueJson; + +// 4. All UrlQueries (locale, skip, limit, includes, projections, etc.) +foreach (var kvp in UrlQueries) + mainJson[kvp.Key] = kvp.Value; +``` + +### How QueryValueJson Is Serialized + +`Dictionary` values are JSON-serialized by `HttpRequestHandler`: + +``` +QueryValueJson = { "title": {"$ne": "Draft"}, "color": {"$in": ["red","blue"]} } +→ query={"title":{"$ne":"Draft"},"color":{"$in":["red","blue"]}} +``` + +This becomes a single `query=` URL parameter with the JSON as its value. + +### Sync API Patterns + +```csharp +// Initial sync (all published content) +SyncStack result = await client.Sync(new SyncStack() { Type = SyncType.entry_published }); + +// Paginated initial sync +SyncStack result = await client.SyncRecursive(parameters); +// result.PaginationToken — continue paginating if not null +// result.SyncToken — use for next delta sync + +// Delta sync (changes since last sync) +SyncStack delta = await client.SyncToken(result.SyncToken); + +// Manual pagination loop +SyncStack page = initialResult; +while (page.PaginationToken != null) +{ + page = await client.SyncPaginationToken(page.PaginationToken); + // process page.Items +} +``` + +### Taxonomy Query Patterns + +```csharp +// Create taxonomy query via client.Taxonomies() +Query taxonomyQuery = client.Taxonomies(); + +// Same filter methods apply +taxonomyQuery.Where("taxonomies.animals", new Dictionary { + { "$eq", "mammals" } +}); + +var results = await taxonomyQuery.Find(); +``` + +**URL paths** + +- Normal content-type query: `{stack.Config.BaseUrl}/content_types/{uid}/entries` +- Taxonomy query (`client.Taxonomies()`): `{stack.Config.BaseUrl}/taxonomies/entries` + +Filter and pagination APIs are the same; only the base path differs. + +### AssetLibrary Query Patterns + +```csharp +AssetLibrary assetLib = client.Assets(); +assetLib.Skip(0).Limit(100); +assetLib.IncludeCount(); +assetLib.SetLocale("en-us"); +ContentstackCollection assets = await assetLib.FetchAll(); +``` + +### Common Mistakes to Avoid + +- Never set `"environment"` key manually in `UrlQueries` — it is always injected from `Config.Environment` in `Exec()` +- Never call `ProcessRequest` directly — always go through model methods (`Find`, `Fetch`, etc.) +- Never modify `QueryValueJson` from outside `Query` — use the public fluent methods +- `And()` / `Or()` take full `Query` instances, not raw dictionaries +- `string[]` values in `UrlQueries` become repeated URL params, not JSON arrays — use for tags, not Mongo operators diff --git a/skills/query-building/references/query-patterns.md b/skills/query-building/references/query-patterns.md deleted file mode 100644 index ec0753ef..00000000 --- a/skills/query-building/references/query-patterns.md +++ /dev/null @@ -1,280 +0,0 @@ -# Query Patterns Reference - -## Quick reference: QueryValueJson operators - -| Method | Operator | Example JSON shape | -|--------|----------|-------------------| -| `Where(key, value)` | direct equality | `{"field": "value"}` | -| `NotEqualTo(key, value)` | `$ne` | `{"field":{"$ne":"x"}}` | -| `ContainedIn(key, values)` | `$in` | `{"field":{"$in":["a","b"]}}` | -| `NotContainedIn(key, values)` | `$nin` | `{"field":{"$nin":["a"]}}` | -| `Exists(key)` | `$exists: true` | `{"field":{"$exists":true}}` | -| `NotExists(key)` | `$exists: false` | `{"field":{"$exists":false}}` | -| `GreaterThan(key, value)` | `$gt` | `{"field":{"$gt":10}}` | -| `LessThan(key, value)` | `$lt` | `{"field":{"$lt":10}}` | -| `Regex(key, pattern)` | `$regex` | `{"field":{"$regex":"^blt"}}` | -| `And(queries[])` | `$and` | array of query objects | -| `Or(queries[])` | `$or` | array of query objects | - -## Quick reference: UrlQueries keys - -| Method | Key | Notes | -|--------|-----|-------| -| `SetLocale(locale)` | `locale` | prefer over obsolete `Language` enum | -| `Skip(n)` | `skip` | pagination offset | -| `Limit(n)` | `limit` | pagination page size | -| `IncludeSchema()` | `include_schema` | `true` | -| `IncludeCount()` | `include_count` | `true` | -| `Tags(tags[])` | `tags` | `string[]` → repeated param | -| `Only(fields[])` | `only[BASE][]` | field projection | -| `Except(fields[])` | `except[BASE][]` | field exclusion | -| `IncludeReference(key)` | `include[]` | reference expansion | - -## Extending Query.cs (new fluent methods) - -### New Mongo-style filter operator - -```csharp -// Pattern used by all existing operators in Query.cs -public Query MyNewOperator(string key, object value) -{ - if (key == null) - throw QueryFilterException.Create(new ArgumentNullException(nameof(key))); - - if (!QueryValueJson.ContainsKey(key)) - QueryValueJson[key] = new Dictionary(); - - ((Dictionary)QueryValueJson[key])["$myop"] = value; - return this; // always return this for fluent chaining -} -``` - -### New URL-level parameter - -```csharp -public Query MyParam(string value) -{ - UrlQueries["my_param"] = value; - return this; -} -``` - -## Terminal operations - -```csharp -Task> result = await query.Find(); -Task result = await query.FindOne(); -Task count = await query.Count(); -``` - -## Paginating Find results (entries) - -```csharp -// ContentstackCollection response shape -result.Items // IEnumerable -result.Count // total count (requires IncludeCount()) -result.Skip // current offset -result.Limit // current page size - -// Paging loop -int skip = 0, limit = 100; -ContentstackCollection page; -do { - page = await query.Skip(skip).Limit(limit).Find(); - // process page.Items - skip += limit; -} while (page.Items.Count() == limit); -``` - -## SyncStack short reference - -When using `SyncRecursive` / sync APIs: - -```csharp -SyncStack syncResult = await client.SyncRecursive(parameters); -// SyncStack.PaginationToken — non-null while more pages exist -// SyncStack.SyncToken — final token for next delta sync -``` - -(See [Sync API Patterns](#sync-api-patterns) below for full sync flows.) - -## Complete Operator Categories - -### Comparison Operators - -```csharp -query.Where("title", "My Entry") // direct equality -query.NotEqualTo("status", "draft") // $ne -query.GreaterThan("price", 100) // $gt -query.GreaterThanEqualTo("price", 100) // $gte -query.LessThan("price", 200) // $lt -query.LessThanEqualTo("price", 200) // $lte -``` - -### Array / Set Operators - -```csharp -query.ContainedIn("color", new[] {"red", "blue"}) // $in -query.NotContainedIn("color", new[] {"green"}) // $nin -``` - -### Existence Operators - -```csharp -query.Exists("field_name") // $exists: true -query.NotExists("field_name") // $exists: false -``` - -### String Operators - -```csharp -query.Regex("uid", "^blt[a-zA-Z0-9]+$") // $regex -query.Regex("title", "^hello", "i") // $regex with $options modifier -``` - -### Logical Operators - -```csharp -// And / Or take Query[] — each sub-query builds its own QueryValueJson -var q1 = client.ContentType("ct").Query().Where("color", "red"); -var q2 = client.ContentType("ct").Query().Where("size", "large"); -query.And(new[] { q1, q2 }); // $and -query.Or(new[] { q1, q2 }); // $or -``` - -## Reference / Include Patterns - -```csharp -query.IncludeReference("reference_field"); // expand single reference -query.IncludeReference(new[] {"ref1", "ref2"}); // expand multiple -query.IncludeSchema(); // include content type schema -query.IncludeCount(); // include total count in response -query.IncludeOwner(); // include entry owner info -query.IncludeMetadata(); // include entry metadata -``` - -## Field Projection - -```csharp -query.Only(new[] {"title", "uid", "price"}); // return only these fields -query.Except(new[] {"body", "image"}); // exclude these fields - -// For referenced fields -query.OnlyWithReferenceUid(new[] {"title"}, "reference_field"); -query.ExceptWithReferenceUid(new[] {"body"}, "reference_field"); -``` - -## Ordering - -```csharp -query.OrderByAscending("title"); -query.OrderByDescending("created_at"); -``` - -## Locale and Environment - -```csharp -query.SetLocale("en-us"); // preferred — string locale code -// Environment is injected automatically from Config in Exec() -// Never set "environment" manually in UrlQueries -``` - -## Exec() Implementation Detail - -The full merge performed in `Query.Exec()` before calling `HttpRequestHandler`: - -```csharp -var mainJson = new Dictionary(); - -// 1. Environment (always injected from Config) -mainJson["environment"] = ContentTypeInstance.StackInstance.Config.Environment; - -// 2. Live preview headers (if enabled and content type matches) -if (livePreviewActive) -{ - mainJson["live_preview"] = livePreviewConfig.LivePreview; - mainJson["authorization"] = livePreviewConfig.ManagementToken; - // or mainJson["preview_token"] = livePreviewConfig.PreviewToken; -} - -// 3. Mongo-style query filter (only if non-empty) -if (QueryValueJson.Count > 0) - mainJson["query"] = QueryValueJson; - -// 4. All UrlQueries (locale, skip, limit, includes, projections, etc.) -foreach (var kvp in UrlQueries) - mainJson[kvp.Key] = kvp.Value; -``` - -## How QueryValueJson Is Serialized - -`Dictionary` values are JSON-serialized by `HttpRequestHandler`: - -``` -QueryValueJson = { "title": {"$ne": "Draft"}, "color": {"$in": ["red","blue"]} } -→ query={"title":{"$ne":"Draft"},"color":{"$in":["red","blue"]}} -``` - -This becomes a single `query=` URL parameter with the JSON as its value. - -## Sync API Patterns - -```csharp -// Initial sync (all published content) -SyncStack result = await client.Sync(new SyncStack() { Type = SyncType.entry_published }); - -// Paginated initial sync -SyncStack result = await client.SyncRecursive(parameters); -// result.PaginationToken — continue paginating if not null -// result.SyncToken — use for next delta sync - -// Delta sync (changes since last sync) -SyncStack delta = await client.SyncToken(result.SyncToken); - -// Manual pagination loop -SyncStack page = initialResult; -while (page.PaginationToken != null) -{ - page = await client.SyncPaginationToken(page.PaginationToken); - // process page.Items -} -``` - -## Taxonomy Query Patterns - -```csharp -// Create taxonomy query via client.Taxonomies() -Query taxonomyQuery = client.Taxonomies(); - -// Same filter methods apply -taxonomyQuery.Where("taxonomies.animals", new Dictionary { - { "$eq", "mammals" } -}); - -var results = await taxonomyQuery.Find(); -``` - -**URL paths** - -- Normal content-type query: `{stack.Config.BaseUrl}/content_types/{uid}/entries` -- Taxonomy query (`client.Taxonomies()`): `{stack.Config.BaseUrl}/taxonomies/entries` - -Filter and pagination APIs are the same; only the base path differs. - -## AssetLibrary Query Patterns - -```csharp -AssetLibrary assetLib = client.Assets(); -assetLib.Skip(0).Limit(100); -assetLib.IncludeCount(); -assetLib.SetLocale("en-us"); -ContentstackCollection assets = await assetLib.FetchAll(); -``` - -## Common Mistakes to Avoid - -- Never set `"environment"` key manually in `UrlQueries` — it is always injected from `Config.Environment` in `Exec()` -- Never call `ProcessRequest` directly — always go through model methods (`Find`, `Fetch`, etc.) -- Never modify `QueryValueJson` from outside `Query` — use the public fluent methods -- `And()` / `Or()` take full `Query` instances, not raw dictionaries -- `string[]` values in `UrlQueries` become repeated URL params, not JSON arrays — use for tags, not Mongo operators diff --git a/skills/sdk-core-patterns/SKILL.md b/skills/sdk-core-patterns/SKILL.md index 8a3ebeff..b6e36e3a 100644 --- a/skills/sdk-core-patterns/SKILL.md +++ b/skills/sdk-core-patterns/SKILL.md @@ -44,11 +44,198 @@ client.Taxonomies(); client.Sync(...); ## Options → Config → BaseUrl -`ContentstackOptions` → internal `Config` → `BaseUrl` (region + host + version). Required: `ApiKey`, `DeliveryToken`, `Environment`. See **[SDK Architecture](references/sdk-architecture.md)** for region table, live preview host switching, and internal client state. +`ContentstackOptions` → internal `Config` → `BaseUrl` (region + host + version). Required: `ApiKey`, `DeliveryToken`, `Environment`. Region table, live preview host switching, and internal client state are in **SDK architecture reference** below. ASP.NET Core registration is covered under **ASP.NET Core integration** below. -## Reference map -| Topic | Document | -|-------|----------| -| Full HTTP flow, Config/BaseUrl, live preview, query serialization, plugins, regions, NuGet layout | [references/sdk-architecture.md](references/sdk-architecture.md) | -| `AddContentstack`, `ContentstackOptions` section, transient registration | [references/aspnetcore-integration.md](references/aspnetcore-integration.md) | +## SDK Architecture Reference + +### Full Request Flow + +``` +ContentstackClient + └── ContentType("uid") → ContentType + └── Query() → Query + └── Find() → Query.Exec() + └── HttpRequestHandler.ProcessRequest(url, headers, bodyJson) + ├── Serialize BodyJson → query string + ├── Create HttpWebRequest (GET) + ├── Set headers (api_key, access_token, branch, x-user-agent) + ├── foreach plugin: OnRequest(client, request) + ├── await request.GetResponseAsync() + ├── foreach plugin: OnResponse(client, request, response, body) + └── return JSON string → parsed in Query.parseJObject +``` + +### Config.BaseUrl Composition + +``` +Protocol Region Code Host Version +"https://" "" "cdn.contentstack.io" "/v3" → US (default) +"https://" "eu-" "cdn.contentstack.com" "/v3" → EU +"https://" "azure-na-" "cdn.contentstack.com" "/v3" → AZURE_NA +"https://" "azure-eu-" "cdn.contentstack.com" "/v3" → AZURE_EU +"https://" "gcp-na-" "cdn.contentstack.com" "/v3" → GCP_NA +"https://" "au-" "cdn.contentstack.com" "/v3" → AU +``` + +`HostURL` defaults to `cdn.contentstack.io` for US, `cdn.contentstack.com` for all other regions. + +### Live Preview URL Resolution + +When `LivePreviewConfig.Enable == true` and `LivePreview != "init"` and `ContentTypeUID` matches the queried content type, `Config.getBaseUrl()` returns the live preview host instead of `BaseUrl`: + +``` +"https://{livePreviewConfig.Host}/{version}" +``` + +Additional headers injected: `live_preview`, `authorization` (management token) or `preview_token`, optional `release_id`, `preview_timestamp`. + +### Query String Serialization Rules (HttpRequestHandler) + +| Value type | Serialization | +|-----------|--------------| +| `string` | `key=value` | +| `string[]` | `key=v1&key=v2` (repeated) | +| `Dictionary` | `key={"$in":["a","b"]}` (JSON) | +| Other | `key=value.ToString()` | + +### ContentstackClient Internal State + +```csharp +internal string StackApiKey // from options +internal Dictionary _Headers // api_key, access_token/delivery_token +internal Dictionary _StackHeaders // shared across requests +internal LivePreviewConfig LivePreviewConfig // null if not configured +public List Plugins // empty by default +public JsonSerializerSettings SerializerSettings // for Fetch/Find +internal JsonSerializer Serializer // created from SerializerSettings +``` + +### How Models Get Stack Context + +All model constructors are `internal`. `ContentstackClient` methods set back-references: + +```csharp +// ContentType.cs internal wiring +internal ContentstackClient StackInstance { get; set; } + +// Query.cs +private ContentType ContentTypeInstance { get; set; } // for entry path +private ContentstackClient TaxonomyInstance { get; set; } // for taxonomy path +``` + +Models build their URL from `ContentTypeInstance.StackInstance.Config.BaseUrl` at call time (lazy). + +### Plugin Implementation Pattern + +```csharp +public class MyPlugin : IContentstackPlugin +{ + public Task OnRequest(ContentstackClient stack, HttpWebRequest request) + { + // Mutate request (add headers, log, etc.) + return Task.FromResult(request); + } + + public Task OnResponse(ContentstackClient stack, HttpWebRequest request, + HttpWebResponse response, string responseString) + { + // Inspect/transform response body string + return Task.FromResult(responseString); + } +} + +// Register +client.Plugins.Add(new MyPlugin()); +``` + +### ContentstackRegion Enum Values + +```csharp +public enum ContentstackRegion { US, EU, AZURE_NA, AZURE_EU, GCP_NA, AU } +``` + +`ContentstackRegionCode` (internal enum) maps to URL prefixes: `eu`, `azure_na`, `azure_eu`, `gcp_na`, `au`. Underscores are replaced with hyphens in the URL. + +### Key NuGet Dependencies (Contentstack.Core.csproj) + +| Package | Version | Purpose | +|---------|---------|---------| +| `Newtonsoft.Json` | 13.0.4 | All JSON serialization | +| `Microsoft.Extensions.Options` | 8.0.2 | `IOptions` | +| `Markdig` | 0.36.2 | Markdown processing in RTE fields | +| `contentstack.utils` | 1.0.6 | RTE embedded item resolution | + +### Solution Layout + +``` +Contentstack.Net.sln +├── Contentstack.Core/ ← Main SDK package (contentstack.csharp on NuGet) +├── Contentstack.AspNetCore/ ← DI extension (contentstack.aspnetcore on NuGet) +├── Contentstack.Core.Tests/ ← Integration tests (net7.0, hits live API) +└── Contentstack.Core.Unit.Tests/ ← Unit tests (no network) +``` + +Version shared via `Directory.Build.props` → `2.26.0` (or current). + +### Supporting internals (maintainers) + +When debugging HTTP, serialization edges, or multi-target behavior, these types in `Contentstack.Core/Internals/` are often involved. They are not public API. + +| Area | Types / files | +|------|----------------| +| Async helpers around `HttpWebRequest` | `WebRequestAsyncExtensions.cs` | +| Version string / user-agent composition | `VersionUtility.cs`, `StackOutput.cs` | +| JSON / value coercion helpers | `ContentstackConvert.cs` | +| Language / locale enums | `LanguageEnums.cs` | + +Prefer changing behavior through `HttpRequestHandler`, `Config`, and public models rather than exposing these types. + +## ASP.NET Core integration + +Source: [`Contentstack.AspNetCore/IServiceCollectionExtensions.cs`](../../../Contentstack.AspNetCore/IServiceCollectionExtensions.cs). + +### Registration + +Two overloads register the same services: + +```csharp +public static IServiceCollection AddContentstack(this IServiceCollection services, IConfigurationRoot configuration) +public static IServiceCollection AddContentstack(this IServiceCollection services, IConfiguration configuration) +``` + +Both: + +1. `services.AddOptions()` +2. `services.Configure(configuration.GetSection("ContentstackOptions"))` +3. `services.TryAddTransient()` + +### Configuration section + +Bind options from configuration using section name **`ContentstackOptions`**: + +```json +{ + "ContentstackOptions": { + "ApiKey": "...", + "DeliveryToken": "...", + "Environment": "..." + } +} +``` + +Adjust property names to match [`ContentstackOptions`](../../../Contentstack.Core/Configuration/ContentstackOptions.cs) public properties. + +### Service lifetime + +`ContentstackClient` is registered as **transient** (`TryAddTransient`). Each resolution gets a new instance; use this when injecting into short-lived scopes or when the app expects a fresh client per operation. + +### Usage in app code + +Inject `ContentstackClient` or `IOptions` as needed after calling `AddContentstack` in `Program.cs` / `Startup.cs`: + +```csharp +services.AddContentstack(configuration); +``` + +Ensure `configuration` includes the `ContentstackOptions` section (e.g. `appsettings.json`, environment variables, user secrets). diff --git a/skills/sdk-core-patterns/references/aspnetcore-integration.md b/skills/sdk-core-patterns/references/aspnetcore-integration.md deleted file mode 100644 index 73ca887a..00000000 --- a/skills/sdk-core-patterns/references/aspnetcore-integration.md +++ /dev/null @@ -1,48 +0,0 @@ -# ASP.NET Core integration - -Source: [`Contentstack.AspNetCore/IServiceCollectionExtensions.cs`](../../../Contentstack.AspNetCore/IServiceCollectionExtensions.cs). - -## Registration - -Two overloads register the same services: - -```csharp -public static IServiceCollection AddContentstack(this IServiceCollection services, IConfigurationRoot configuration) -public static IServiceCollection AddContentstack(this IServiceCollection services, IConfiguration configuration) -``` - -Both: - -1. `services.AddOptions()` -2. `services.Configure(configuration.GetSection("ContentstackOptions"))` -3. `services.TryAddTransient()` - -## Configuration section - -Bind options from configuration using section name **`ContentstackOptions`**: - -```json -{ - "ContentstackOptions": { - "ApiKey": "...", - "DeliveryToken": "...", - "Environment": "..." - } -} -``` - -Adjust property names to match [`ContentstackOptions`](../../../Contentstack.Core/Configuration/ContentstackOptions.cs) public properties. - -## Service lifetime - -`ContentstackClient` is registered as **transient** (`TryAddTransient`). Each resolution gets a new instance; use this when injecting into short-lived scopes or when the app expects a fresh client per operation. - -## Usage in app code - -Inject `ContentstackClient` or `IOptions` as needed after calling `AddContentstack` in `Program.cs` / `Startup.cs`: - -```csharp -services.AddContentstack(configuration); -``` - -Ensure `configuration` includes the `ContentstackOptions` section (e.g. `appsettings.json`, environment variables, user secrets). diff --git a/skills/sdk-core-patterns/references/sdk-architecture.md b/skills/sdk-core-patterns/references/sdk-architecture.md deleted file mode 100644 index 8eb5bde5..00000000 --- a/skills/sdk-core-patterns/references/sdk-architecture.md +++ /dev/null @@ -1,143 +0,0 @@ -# SDK Architecture Reference - -## Full Request Flow - -``` -ContentstackClient - └── ContentType("uid") → ContentType - └── Query() → Query - └── Find() → Query.Exec() - └── HttpRequestHandler.ProcessRequest(url, headers, bodyJson) - ├── Serialize BodyJson → query string - ├── Create HttpWebRequest (GET) - ├── Set headers (api_key, access_token, branch, x-user-agent) - ├── foreach plugin: OnRequest(client, request) - ├── await request.GetResponseAsync() - ├── foreach plugin: OnResponse(client, request, response, body) - └── return JSON string → parsed in Query.parseJObject -``` - -## Config.BaseUrl Composition - -``` -Protocol Region Code Host Version -"https://" "" "cdn.contentstack.io" "/v3" → US (default) -"https://" "eu-" "cdn.contentstack.com" "/v3" → EU -"https://" "azure-na-" "cdn.contentstack.com" "/v3" → AZURE_NA -"https://" "azure-eu-" "cdn.contentstack.com" "/v3" → AZURE_EU -"https://" "gcp-na-" "cdn.contentstack.com" "/v3" → GCP_NA -"https://" "au-" "cdn.contentstack.com" "/v3" → AU -``` - -`HostURL` defaults to `cdn.contentstack.io` for US, `cdn.contentstack.com` for all other regions. - -## Live Preview URL Resolution - -When `LivePreviewConfig.Enable == true` and `LivePreview != "init"` and `ContentTypeUID` matches the queried content type, `Config.getBaseUrl()` returns the live preview host instead of `BaseUrl`: - -``` -"https://{livePreviewConfig.Host}/{version}" -``` - -Additional headers injected: `live_preview`, `authorization` (management token) or `preview_token`, optional `release_id`, `preview_timestamp`. - -## Query String Serialization Rules (HttpRequestHandler) - -| Value type | Serialization | -|-----------|--------------| -| `string` | `key=value` | -| `string[]` | `key=v1&key=v2` (repeated) | -| `Dictionary` | `key={"$in":["a","b"]}` (JSON) | -| Other | `key=value.ToString()` | - -## ContentstackClient Internal State - -```csharp -internal string StackApiKey // from options -internal Dictionary _Headers // api_key, access_token/delivery_token -internal Dictionary _StackHeaders // shared across requests -internal LivePreviewConfig LivePreviewConfig // null if not configured -public List Plugins // empty by default -public JsonSerializerSettings SerializerSettings // for Fetch/Find -internal JsonSerializer Serializer // created from SerializerSettings -``` - -## How Models Get Stack Context - -All model constructors are `internal`. `ContentstackClient` methods set back-references: - -```csharp -// ContentType.cs internal wiring -internal ContentstackClient StackInstance { get; set; } - -// Query.cs -private ContentType ContentTypeInstance { get; set; } // for entry path -private ContentstackClient TaxonomyInstance { get; set; } // for taxonomy path -``` - -Models build their URL from `ContentTypeInstance.StackInstance.Config.BaseUrl` at call time (lazy). - -## Plugin Implementation Pattern - -```csharp -public class MyPlugin : IContentstackPlugin -{ - public Task OnRequest(ContentstackClient stack, HttpWebRequest request) - { - // Mutate request (add headers, log, etc.) - return Task.FromResult(request); - } - - public Task OnResponse(ContentstackClient stack, HttpWebRequest request, - HttpWebResponse response, string responseString) - { - // Inspect/transform response body string - return Task.FromResult(responseString); - } -} - -// Register -client.Plugins.Add(new MyPlugin()); -``` - -## ContentstackRegion Enum Values - -```csharp -public enum ContentstackRegion { US, EU, AZURE_NA, AZURE_EU, GCP_NA, AU } -``` - -`ContentstackRegionCode` (internal enum) maps to URL prefixes: `eu`, `azure_na`, `azure_eu`, `gcp_na`, `au`. Underscores are replaced with hyphens in the URL. - -## Key NuGet Dependencies (Contentstack.Core.csproj) - -| Package | Version | Purpose | -|---------|---------|---------| -| `Newtonsoft.Json` | 13.0.4 | All JSON serialization | -| `Microsoft.Extensions.Options` | 8.0.2 | `IOptions` | -| `Markdig` | 0.36.2 | Markdown processing in RTE fields | -| `contentstack.utils` | 1.0.6 | RTE embedded item resolution | - -## Solution Layout - -``` -Contentstack.Net.sln -├── Contentstack.Core/ ← Main SDK package (contentstack.csharp on NuGet) -├── Contentstack.AspNetCore/ ← DI extension (contentstack.aspnetcore on NuGet) -├── Contentstack.Core.Tests/ ← Integration tests (net7.0, hits live API) -└── Contentstack.Core.Unit.Tests/ ← Unit tests (no network) -``` - -Version shared via `Directory.Build.props` → `2.26.0` (or current). - -## Supporting internals (maintainers) - -When debugging HTTP, serialization edges, or multi-target behavior, these types in `Contentstack.Core/Internals/` are often involved. They are not public API. - -| Area | Types / files | -|------|----------------| -| Async helpers around `HttpWebRequest` | `WebRequestAsyncExtensions.cs` | -| Version string / user-agent composition | `VersionUtility.cs`, `StackOutput.cs` | -| JSON / value coercion helpers | `ContentstackConvert.cs` | -| Language / locale enums | `LanguageEnums.cs` | - -Prefer changing behavior through `HttpRequestHandler`, `Config`, and public models rather than exposing these types. diff --git a/skills/testing/SKILL.md b/skills/testing/SKILL.md index 0f9c278a..e76b7bce 100644 --- a/skills/testing/SKILL.md +++ b/skills/testing/SKILL.md @@ -23,6 +23,312 @@ description: Testing patterns for the Contentstack .NET SDK — unit tests using - Integration tests: **never commit secrets** — credentials from `app.config` / env locally. - New `Query` methods: unit test operators + null/invalid cases; integration test when behavior is API-bound. -## Reference +## Testing Patterns Reference -Templates, reflection helpers, `IntegrationTestBase`, `TestDataHelper`, mocks, traits, `app.config` keys, dotnet CLI filters, RequestLoggingPlugin, coverage guidelines: **[references/testing-patterns.md](references/testing-patterns.md)**. +### Complete Unit Test Template + +```csharp +using System.Collections.Generic; +using System.Reflection; +using AutoFixture; +using Contentstack.Core; +using Contentstack.Core.Configuration; +using Contentstack.Core.Models; +using Contentstack.Core.Internals; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Contentstack.Core.Unit.Tests +{ + public class MyFeatureUnitTests + { + private readonly IFixture _fixture = new Fixture(); + private ContentstackClient _client; + + public MyFeatureUnitTests() + { + var options = new ContentstackOptions() + { + ApiKey = _fixture.Create(), + DeliveryToken = _fixture.Create(), + Environment = _fixture.Create() + }; + _client = new ContentstackClient(new OptionsWrapper(options)); + } + + private Query CreateQuery(string contentTypeId = "source") + => _client.ContentType(contentTypeId).Query(); + + // Helper: get private QueryValueJson + private Dictionary GetQueryValueJson(Query query) + { + var field = typeof(Query).GetField("QueryValueJson", + BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy); + return (Dictionary)field?.GetValue(query); + } + + // Helper: get private UrlQueries + private Dictionary GetUrlQueries(Query query) + { + var field = typeof(Query).GetField("UrlQueries", + BindingFlags.NonPublic | BindingFlags.Instance); + return (Dictionary)field?.GetValue(query); + } + + [Fact] + public void MyOperator_AddsCorrectQueryParameter() + { + var query = CreateQuery(); + var key = _fixture.Create(); + + var result = query.MyOperator(key, "value"); + + Assert.Equal(query, result); // fluent return + var qvj = GetQueryValueJson(query); + Assert.True(qvj.ContainsKey(key)); + var inner = qvj[key] as Dictionary; + Assert.True(inner.ContainsKey("$myop")); + Assert.Equal("value", inner["$myop"]); + } + + [Fact] + public void MyUrlParam_AddsToUrlQueries() + { + var query = CreateQuery(); + + query.SetLocale("en-us"); + + var urlQueries = GetUrlQueries(query); + Assert.Equal("en-us", urlQueries["locale"]); + } + + [Fact] + public void MyOperator_WithNullKey_ThrowsQueryFilterException() + { + var query = CreateQuery(); + Assert.Throws(() => query.MyOperator(null, "value")); + } + + [Fact] + public void MyOperator_WithEmptyKey_ThrowsQueryFilterException() + { + var query = CreateQuery(); + Assert.Throws(() => query.MyOperator(string.Empty, "value")); + } + + [Fact] + public void MyOperator_ReturnsQueryForChaining() + { + var query = CreateQuery(); + var result = query + .MyOperator("field1", "value1") + .MyOperator("field2", "value2"); + Assert.Equal(query, result); + } + } +} +``` + +### Complete Integration Test Template + +```csharp +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; +using Contentstack.Core.Models; +using Contentstack.Core.Tests.Helpers; + +namespace Contentstack.Core.Tests.Integration.MyFeatureTests +{ + public class MyFeatureComprehensiveTest : IntegrationTestBase + { + public MyFeatureComprehensiveTest(ITestOutputHelper output) : base(output) + { + } + + [Fact(DisplayName = "MyFeature - BasicOperation ReturnsExpectedResult")] + public async Task MyFeature_BasicOperation_ReturnsExpectedResult() + { + // Arrange + LogArrange("Setting up basic operation test"); + LogContext("ContentType", TestDataHelper.SimpleContentTypeUid); + + var client = CreateClient(); + var query = client.ContentType(TestDataHelper.SimpleContentTypeUid).Query(); + + // Act + LogAct("Executing query with my feature"); + query.MyOperator("uid", "someValue"); + var result = await query.Find(); + + // Assert + LogAssert("Verifying response structure"); + TestAssert.NotNull(result); + TestAssert.NotNull(result.Items); + TestAssert.True(result.Count >= 0, "Count should be non-negative"); + } + + [Fact(DisplayName = "MyFeature - WithInvalidParams ThrowsException")] + public async Task MyFeature_WithInvalidParams_ThrowsException() + { + // Arrange + LogArrange("Setting up error scenario"); + var client = CreateClient(); + + // Act & Assert + LogAct("Executing with invalid parameters"); + await Assert.ThrowsAsync(async () => + { + await client.ContentType("nonexistent_type_12345") + .Query().Find(); + }); + } + } +} +``` + +### Existing Unit Test Files — What Each Covers + +| File | Covers | +|------|--------| +| `QueryUnitTests.cs` | All `Query` filter/operator methods, UrlQueries params | +| `EntryUnitTests.cs` | `Entry` field access, URL construction, header setting | +| `AssetUnitTests.cs` | `Asset` model fields | +| `AssetLibraryUnitTests.cs` | `AssetLibrary` query params, Skip/Limit | +| `ContentstackClientUnitTests.cs` | Client initialization, header injection, factory methods | +| `ContentstackOptionsUnitTests.cs` | Options defaults, validation | +| `ContentstackExceptionUnitTests.cs` | Exception hierarchy, factory methods, message content | +| `ConfigUnitTests.cs` | BaseUrl composition, region codes | +| `ContentstackRegionUnitTests.cs` | Region enum → URL prefix mapping | +| `GlobalFieldUnitTests.cs` | GlobalField ID validation, URL construction | +| `GlobalFieldQueryUnitTests.cs` | GlobalFieldQuery filter methods | +| `TaxonomyUnitTests.cs` | Taxonomy query path | +| `JsonConverterUnitTests.cs` | CSJsonConverter attribute registration | +| `LivePreviewConfigUnitTests.cs` | LivePreviewConfig validation | + +### Existing Integration Test Folders — What Each Covers + +| Folder | Covers | +|--------|--------| +| `QueryTests/` | Query operators, complex filters, field queries, includes | +| `EntryTests/` | Entry fetch, field projection, references | +| `GlobalFieldsTests/` | Global field schemas, nested global fields | +| `SyncTests/` | Sync API, pagination tokens, delta sync | +| `AssetTests/` | Asset fetch, asset library queries | +| `ContentTypeTests/` | Content type fetch, schema queries | +| `LocalizationTests/` | Locale filtering, locale fallback chains | +| `PaginationTests/` | Skip/Limit behavior, count accuracy | +| `ErrorHandling/` | API error codes, exception types, invalid params | +| `LivePreview/` | Live preview URL routing, token headers | +| `ModularBlocksTests/` | Modular block field deserialization | +| `MetadataTests/` | Entry metadata fields | +| `TaxonomyTests/` | Taxonomy query path, taxonomy filtering | +| `VariantsTests/` | Entry variant headers, variant content | +| `BranchTests/` | Branch header injection | + +### MockHttpHandler Pattern (Unit Tests) + +When you need to mock HTTP responses without network: + +```csharp +// In Mokes/MockHttpHandler.cs — extend for new mock scenarios +// MockResponse.cs — add JSON fixture strings for new response shapes +// MockInfrastructureTest.cs — base class wiring MockHttpHandler into client +``` + +### TestAssert Wrappers + +Use `TestAssert.*` instead of raw `Assert.*` in integration tests — they log assertion context to `ITestOutputHelper`: + +```csharp +TestAssert.NotNull(result); +TestAssert.Equal(expected, actual); +TestAssert.True(condition, "failure message"); +TestAssert.False(condition, "failure message"); +TestAssert.IsAssignableFrom>(result.Items); +TestAssert.Matches("^blt[a-zA-Z0-9]+$", entry.Uid); +``` + +### app.config Keys for Integration Tests + +Integration tests read config from `Contentstack.Core.Tests/app.config`: + +```xml + + + + + + + + + + +``` + +Never commit real credentials. Use environment variables or a secrets manager in CI. + +### Running Tests + +```bash +# Unit tests only (no credentials needed) +dotnet test Contentstack.Core.Unit.Tests/ + +# Integration tests (requires app.config with valid credentials) +dotnet test Contentstack.Core.Tests/ + +# Run specific category +dotnet test --filter "Category=RetryIntegration" + +# Run specific test class +dotnet test --filter "FullyQualifiedName~QueryOperatorsComprehensiveTest" +``` + +### Mokes folder (unit tests) + +`Contentstack.Core.Unit.Tests/Mokes/`: + +- `MockHttpHandler.cs` — intercepts HTTP without network +- `MockResponse.cs` — sample JSON response fixtures +- `MockInfrastructureTest.cs` — base for tests needing mock HTTP +- `Utilities.cs` — test utility helpers + +### RequestLoggingPlugin (integration) + +`CreateClient()` on `IntegrationTestBase` adds `RequestLoggingPlugin`, which logs HTTP requests and responses via `ITestOutputHelper`. No extra setup required. + +Custom plugins for a test: + +```csharp +var client = CreateClient(); +client.Plugins.Add(new MyTestPlugin()); +``` + +### Test coverage guidelines + +- Unit test: every new public `Query` method (operator or URL param) +- Unit test: null/invalid input → expected exception type +- Integration test: happy path with real API response +- Integration test: verify response shape (`Items`, `Count`, fields) +- Place integration tests under the folder that matches the feature area + +### Integration test file conventions + +- Folders mirror features: `Integration/QueryTests/`, `Integration/EntryTests/`, `Integration/GlobalFieldsTests/`, etc. +- One test class per broad concern when it makes sense; file names often end in `Test.cs` / `Tests.cs` +- Use `LogArrange` / `LogAct` / `LogAssert` / `LogContext` from `IntegrationTestBase` (see templates above) + +#### xUnit traits (examples) + +```csharp +[Trait("Category", "RetryIntegration")] +[Trait("Category", "LivePreview")] +[Trait("Category", "Sync")] +``` + +#### DisplayName convention + +```csharp +[Fact(DisplayName = "Query Operations - Regex Complex Pattern Matches Correctly")] +// FeatureArea - ComponentAction Outcome +``` diff --git a/skills/testing/references/testing-patterns.md b/skills/testing/references/testing-patterns.md deleted file mode 100644 index d83505da..00000000 --- a/skills/testing/references/testing-patterns.md +++ /dev/null @@ -1,309 +0,0 @@ -# Testing Patterns Reference - -## Complete Unit Test Template - -```csharp -using System.Collections.Generic; -using System.Reflection; -using AutoFixture; -using Contentstack.Core; -using Contentstack.Core.Configuration; -using Contentstack.Core.Models; -using Contentstack.Core.Internals; -using Microsoft.Extensions.Options; -using Xunit; - -namespace Contentstack.Core.Unit.Tests -{ - public class MyFeatureUnitTests - { - private readonly IFixture _fixture = new Fixture(); - private ContentstackClient _client; - - public MyFeatureUnitTests() - { - var options = new ContentstackOptions() - { - ApiKey = _fixture.Create(), - DeliveryToken = _fixture.Create(), - Environment = _fixture.Create() - }; - _client = new ContentstackClient(new OptionsWrapper(options)); - } - - private Query CreateQuery(string contentTypeId = "source") - => _client.ContentType(contentTypeId).Query(); - - // Helper: get private QueryValueJson - private Dictionary GetQueryValueJson(Query query) - { - var field = typeof(Query).GetField("QueryValueJson", - BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy); - return (Dictionary)field?.GetValue(query); - } - - // Helper: get private UrlQueries - private Dictionary GetUrlQueries(Query query) - { - var field = typeof(Query).GetField("UrlQueries", - BindingFlags.NonPublic | BindingFlags.Instance); - return (Dictionary)field?.GetValue(query); - } - - [Fact] - public void MyOperator_AddsCorrectQueryParameter() - { - var query = CreateQuery(); - var key = _fixture.Create(); - - var result = query.MyOperator(key, "value"); - - Assert.Equal(query, result); // fluent return - var qvj = GetQueryValueJson(query); - Assert.True(qvj.ContainsKey(key)); - var inner = qvj[key] as Dictionary; - Assert.True(inner.ContainsKey("$myop")); - Assert.Equal("value", inner["$myop"]); - } - - [Fact] - public void MyUrlParam_AddsToUrlQueries() - { - var query = CreateQuery(); - - query.SetLocale("en-us"); - - var urlQueries = GetUrlQueries(query); - Assert.Equal("en-us", urlQueries["locale"]); - } - - [Fact] - public void MyOperator_WithNullKey_ThrowsQueryFilterException() - { - var query = CreateQuery(); - Assert.Throws(() => query.MyOperator(null, "value")); - } - - [Fact] - public void MyOperator_WithEmptyKey_ThrowsQueryFilterException() - { - var query = CreateQuery(); - Assert.Throws(() => query.MyOperator(string.Empty, "value")); - } - - [Fact] - public void MyOperator_ReturnsQueryForChaining() - { - var query = CreateQuery(); - var result = query - .MyOperator("field1", "value1") - .MyOperator("field2", "value2"); - Assert.Equal(query, result); - } - } -} -``` - -## Complete Integration Test Template - -```csharp -using System.Threading.Tasks; -using Xunit; -using Xunit.Abstractions; -using Contentstack.Core.Models; -using Contentstack.Core.Tests.Helpers; - -namespace Contentstack.Core.Tests.Integration.MyFeatureTests -{ - public class MyFeatureComprehensiveTest : IntegrationTestBase - { - public MyFeatureComprehensiveTest(ITestOutputHelper output) : base(output) - { - } - - [Fact(DisplayName = "MyFeature - BasicOperation ReturnsExpectedResult")] - public async Task MyFeature_BasicOperation_ReturnsExpectedResult() - { - // Arrange - LogArrange("Setting up basic operation test"); - LogContext("ContentType", TestDataHelper.SimpleContentTypeUid); - - var client = CreateClient(); - var query = client.ContentType(TestDataHelper.SimpleContentTypeUid).Query(); - - // Act - LogAct("Executing query with my feature"); - query.MyOperator("uid", "someValue"); - var result = await query.Find(); - - // Assert - LogAssert("Verifying response structure"); - TestAssert.NotNull(result); - TestAssert.NotNull(result.Items); - TestAssert.True(result.Count >= 0, "Count should be non-negative"); - } - - [Fact(DisplayName = "MyFeature - WithInvalidParams ThrowsException")] - public async Task MyFeature_WithInvalidParams_ThrowsException() - { - // Arrange - LogArrange("Setting up error scenario"); - var client = CreateClient(); - - // Act & Assert - LogAct("Executing with invalid parameters"); - await Assert.ThrowsAsync(async () => - { - await client.ContentType("nonexistent_type_12345") - .Query().Find(); - }); - } - } -} -``` - -## Existing Unit Test Files — What Each Covers - -| File | Covers | -|------|--------| -| `QueryUnitTests.cs` | All `Query` filter/operator methods, UrlQueries params | -| `EntryUnitTests.cs` | `Entry` field access, URL construction, header setting | -| `AssetUnitTests.cs` | `Asset` model fields | -| `AssetLibraryUnitTests.cs` | `AssetLibrary` query params, Skip/Limit | -| `ContentstackClientUnitTests.cs` | Client initialization, header injection, factory methods | -| `ContentstackOptionsUnitTests.cs` | Options defaults, validation | -| `ContentstackExceptionUnitTests.cs` | Exception hierarchy, factory methods, message content | -| `ConfigUnitTests.cs` | BaseUrl composition, region codes | -| `ContentstackRegionUnitTests.cs` | Region enum → URL prefix mapping | -| `GlobalFieldUnitTests.cs` | GlobalField ID validation, URL construction | -| `GlobalFieldQueryUnitTests.cs` | GlobalFieldQuery filter methods | -| `TaxonomyUnitTests.cs` | Taxonomy query path | -| `JsonConverterUnitTests.cs` | CSJsonConverter attribute registration | -| `LivePreviewConfigUnitTests.cs` | LivePreviewConfig validation | - -## Existing Integration Test Folders — What Each Covers - -| Folder | Covers | -|--------|--------| -| `QueryTests/` | Query operators, complex filters, field queries, includes | -| `EntryTests/` | Entry fetch, field projection, references | -| `GlobalFieldsTests/` | Global field schemas, nested global fields | -| `SyncTests/` | Sync API, pagination tokens, delta sync | -| `AssetTests/` | Asset fetch, asset library queries | -| `ContentTypeTests/` | Content type fetch, schema queries | -| `LocalizationTests/` | Locale filtering, locale fallback chains | -| `PaginationTests/` | Skip/Limit behavior, count accuracy | -| `ErrorHandling/` | API error codes, exception types, invalid params | -| `LivePreview/` | Live preview URL routing, token headers | -| `ModularBlocksTests/` | Modular block field deserialization | -| `MetadataTests/` | Entry metadata fields | -| `TaxonomyTests/` | Taxonomy query path, taxonomy filtering | -| `VariantsTests/` | Entry variant headers, variant content | -| `BranchTests/` | Branch header injection | - -## MockHttpHandler Pattern (Unit Tests) - -When you need to mock HTTP responses without network: - -```csharp -// In Mokes/MockHttpHandler.cs — extend for new mock scenarios -// MockResponse.cs — add JSON fixture strings for new response shapes -// MockInfrastructureTest.cs — base class wiring MockHttpHandler into client -``` - -## TestAssert Wrappers - -Use `TestAssert.*` instead of raw `Assert.*` in integration tests — they log assertion context to `ITestOutputHelper`: - -```csharp -TestAssert.NotNull(result); -TestAssert.Equal(expected, actual); -TestAssert.True(condition, "failure message"); -TestAssert.False(condition, "failure message"); -TestAssert.IsAssignableFrom>(result.Items); -TestAssert.Matches("^blt[a-zA-Z0-9]+$", entry.Uid); -``` - -## app.config Keys for Integration Tests - -Integration tests read config from `Contentstack.Core.Tests/app.config`: - -```xml - - - - - - - - - - -``` - -Never commit real credentials. Use environment variables or a secrets manager in CI. - -## Running Tests - -```bash -# Unit tests only (no credentials needed) -dotnet test Contentstack.Core.Unit.Tests/ - -# Integration tests (requires app.config with valid credentials) -dotnet test Contentstack.Core.Tests/ - -# Run specific category -dotnet test --filter "Category=RetryIntegration" - -# Run specific test class -dotnet test --filter "FullyQualifiedName~QueryOperatorsComprehensiveTest" -``` - -## Mokes folder (unit tests) - -`Contentstack.Core.Unit.Tests/Mokes/`: - -- `MockHttpHandler.cs` — intercepts HTTP without network -- `MockResponse.cs` — sample JSON response fixtures -- `MockInfrastructureTest.cs` — base for tests needing mock HTTP -- `Utilities.cs` — test utility helpers - -## RequestLoggingPlugin (integration) - -`CreateClient()` on `IntegrationTestBase` adds `RequestLoggingPlugin`, which logs HTTP requests and responses via `ITestOutputHelper`. No extra setup required. - -Custom plugins for a test: - -```csharp -var client = CreateClient(); -client.Plugins.Add(new MyTestPlugin()); -``` - -## Test coverage guidelines - -- Unit test: every new public `Query` method (operator or URL param) -- Unit test: null/invalid input → expected exception type -- Integration test: happy path with real API response -- Integration test: verify response shape (`Items`, `Count`, fields) -- Place integration tests under the folder that matches the feature area - -## Integration test file conventions - -- Folders mirror features: `Integration/QueryTests/`, `Integration/EntryTests/`, `Integration/GlobalFieldsTests/`, etc. -- One test class per broad concern when it makes sense; file names often end in `Test.cs` / `Tests.cs` -- Use `LogArrange` / `LogAct` / `LogAssert` / `LogContext` from `IntegrationTestBase` (see templates above) - -### xUnit traits (examples) - -```csharp -[Trait("Category", "RetryIntegration")] -[Trait("Category", "LivePreview")] -[Trait("Category", "Sync")] -``` - -### DisplayName convention - -```csharp -[Fact(DisplayName = "Query Operations - Regex Complex Pattern Matches Correctly")] -// FeatureArea - ComponentAction Outcome -```