Skip to content

feat(dotnet): support multiple configurable REST resources#108

Open
colbytimm wants to merge 6 commits into
mainfrom
claude/multi-endpoint-dotnet
Open

feat(dotnet): support multiple configurable REST resources#108
colbytimm wants to merge 6 commits into
mainfrom
claude/multi-endpoint-dotnet

Conversation

@colbytimm

@colbytimm colbytimm commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Summary

Ports the multi-resource capability (already merged for the Python and TypeScript templates) to the .NET template. A resources list in copier.yml now drives the generated layers so a single generated project can expose N resources, each with:

  • its own URL endpoint,
  • a storage container (resources sharing a container id share storage),
  • a chosen subset of operations (list, get_by_id, create, update=PATCH, replace=PUT [new], delete).

The default value is a single resource matching the project name with today's CRUD behaviour, so answering only project_name reproduces the original single-resource project (no PUT). PUT/replace is opt-in.

Approach — per-resource named types

Each resource generates its own named types — Cat, CatController, CatService, CatRepository, CatDto, CreateCatRequest, ICatController, … — rather than a shared generic Item* set. Because Copier cannot fan a resource out into separate files, all per-resource classes for a layer live in one file behind a {% for resource in resources %} loop.

  • Controllers & services expose exactly one method per operation the resource declares (per-op {% if "op" in resource.operations %} gating on both the interface and the implementation). Repositories stay a full data-access layer (all six operations) regardless of the subset.
  • ReplaceAsync (full-document overwrite) is implemented across Cosmos DB, Firestore, and DynamoDB.

Changes

  • dotnet/copier.yml — add a validated resources question (PascalCase name, kebab-case endpoint, valid container id, ≥1 valid operation, unique names/endpoints).
  • LayersModels (entities/DTOs/requests/validators), Interfaces, Controllers, Services, Repositories loop over resources and emit a named class/interface per resource; controller/service methods and their request types are gated per operation.
  • WiringDependencyInjection registers a per-resource I<Resource>Controller, each bound to its storage container (Cosmos container / Firestore collection / DynamoDB table) resolved by name from settings; resources sharing a container id resolve the same handle.
  • Per-cloud routing (in-file Jinja loops):
    • AzureItemFunctions injects per-resource controllers and exposes looped [Function] methods (Get, GetList, Create, Update, Replace, Delete) + Health.
    • GCPFunction.cs injects per-resource controllers and dispatches by endpoint → method → id-presence, with per-operation 405 gating.
    • AWSItemFunctions injects per-resource controllers; template.yaml emits one AWS::Serverless::Function per resource×operation, one DynamoDB table per distinct container, plus a Health function.
  • Per-container settings — Azure CosmosDbContainerName_<Container>, GCP FIRESTORE_COLLECTION_<Container>, AWS DYNAMODB_TABLE_NAME_<CONTAINER>.
  • Tests — unit tests loop per resource with per-operation gating (controllers/services); repository tests stay full.
  • README — endpoint list loops over the configured resources/operations.

Verification

Rendered all 3 clouds × 3 fixtures (single default; two resources / separate containers / mixed ops incl. PUT; two resources sharing one container with different op subsets) with copier copy --trust:

  • All 9 renders succeed; all generated .cs files are brace/paren-balanced; no residual generic Item* domain types leak into output (the host wiring class keeps the internal ItemFunctions name, referenced by the AWS handler strings).
  • Operation subsetting is exact per fixture (controller/service expose only the chosen operations; GCP returns 405 for disabled methods; SAM emits only the chosen functions).
  • Shared container ⇒ both resources resolve the same container/collection/table env var; separate ⇒ distinct ones.
  • Default fixture renders types named after the project with the legacy 5 operations (no PUT), staying behaviourally equivalent to the pre-feature output.
  • The validator rejects invalid input (e.g. a non-PascalCase resource name).

No local .NET SDK is available in this environment, so the build/test is validated via the build-dotnet CI pipeline on this PR.

🤖 Generated with Claude Code


Generated by Claude Code

claude added 6 commits July 2, 2026 06:01
Port the multi-resource capability (already in the Python and TypeScript
templates) to the .NET template. A `resources` list in copier.yml drives
generic, registry-wired layers so one generated project can expose N
resources, each with its own endpoint, a storage container (shared when
ids match), and a chosen subset of operations (list, get_by_id, create,
update=PATCH, replace=PUT [new], delete).

- copier.yml: add validated `resources` question (PascalCase name,
  kebab endpoint, container id, valid ops, unique names/endpoints);
  default is a single resource reproducing today's CRUD behaviour.
- Collapse per-resource, name-templated classes into one generic Item*
  set (controller/service/repository/interfaces/models) and add
  ReplaceAsync (full overwrite) across Cosmos/Firestore/DynamoDB.
- DependencyInjection builds a per-container IItemController map keyed by
  container id, resolving each container/collection/table by name.
- Per-cloud routing via in-file Jinja loops: Azure one ItemFunctions with
  looped [Function] methods; GCP Function.cs ResourceRoute dispatch; AWS
  ItemFunctions handlers + SAM template.yaml functions/tables per resource.
- Per-container settings (local.settings.json, template.yaml, env vars).
- Retarget unit tests to the generic Item* classes; drop per-op Functions
  tests. Update README endpoint list to loop over resources/operations.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FHtZ26sF1zNNrEh1wDGzBa
Replace the generic Item* layers with per-resource named types (Cat,
CatController, CatService, CatRepository, CatDto, ICatController, etc.),
mirroring the Go/Python/TypeScript reworks. Controllers and services expose
exactly one method per operation each resource declares; repositories stay a
full data-access layer.

- Models/Interfaces/Controllers/Services/Repositories loop over resources and
  generate a named class/interface per resource; controller & service methods
  and their request types are gated per operation.
- DependencyInjection registers a per-resource I<Resource>Controller, each
  bound to its storage container (Cosmos container / Firestore collection /
  DynamoDB table); resources sharing a container id resolve the same handle.
- Azure & AWS ItemFunctions inject per-resource controllers instead of a
  container-keyed IReadOnlyDictionary; GCP Function dispatches per resource
  with per-operation 405 gating.
- Tests loop per resource with per-operation gating; repository tests stay full.

The single-resource default renders named after the project with the five
legacy operations, staying behaviourally equivalent to the pre-feature output.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FHtZ26sF1zNNrEh1wDGzBa
Add a Publish Example Branches workflow that renders every combination of
language (go/python/typescript/dotnet) x cloud (Azure/GCP/AWS) x resources
fixture (single/multi) with Copier and force-pushes each generated project
to its own examples/<language>-<cloud>-<fixture> branch, so the generated
output is browsable without running Copier locally.

- Extend the setup-copier-template composite action with optional
  `resources-fixture` (single|multi) and `output-dir` inputs; existing build
  pipelines keep their defaults (single / KittenClaws) unchanged.
- Add a multi-resource showcase fixture (two resources sharing one container
  with different operation subsets — Cat via PATCH, Dog via PUT).
- Prefer an EXAMPLES_PUBLISH_TOKEN PAT for full-fidelity publishing; without
  it, fall back to GITHUB_TOKEN and relocate each generated project's
  .github/workflows to .github/workflows-example (the default token cannot
  push workflow files).

Triggers on pushes to main; temporarily also on this feature branch for
verification (the TEMP branch trigger should be removed before/after merge).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FHtZ26sF1zNNrEh1wDGzBa
Rename the published branch prefix from examples/ to example/
(example/<language>-<cloud>-<fixture>).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FHtZ26sF1zNNrEh1wDGzBa
Add a temporary one-shot job that deletes the deprecated examples/* (plural)
branches via the GitHub refs API, following the rename to example/*. The job
is a no-op once the old branches are gone and should be removed afterward.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FHtZ26sF1zNNrEh1wDGzBa
Add a pull_request trigger and a resolve job so each PR publishes example
branches for the language(s) it actually changes, named
example/pr-<number>-<language>-<cloud>-<fixture> and rendered from the PR head.
Pushes to main still publish the canonical example/<language>-<cloud>-<fixture>
set (all four languages).

Also drop the temporary feature-branch push trigger and the one-shot
legacy-examples prune job (its cleanup is complete).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FHtZ26sF1zNNrEh1wDGzBa
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants