Compare commits
242
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09acba310f | ||
|
|
93a13648b7 | ||
|
|
417eae32a7 | ||
|
|
c45813bebd | ||
|
|
074d2ea9f0 | ||
|
|
c45a79bcd7 | ||
|
|
c6140ed807 | ||
|
|
1eb0ec9755 | ||
|
|
a50397f4e6 | ||
|
|
c7ba6506aa | ||
|
|
96ae2f384f | ||
|
|
f158e6601b | ||
|
|
8e6e0e7f05 | ||
|
|
d0648ac7df | ||
|
|
c257b91443 | ||
|
|
6866a964f6 | ||
|
|
233865a1e6 | ||
|
|
877e93aec2 | ||
|
|
adf02910ab | ||
|
|
89e89a38ab | ||
|
|
fa123444bd | ||
|
|
5833269196 | ||
|
|
0c9b817372 | ||
|
|
29ecd48cc1 | ||
|
|
6c2473aeb0 | ||
|
|
7e2edd4733 | ||
|
|
0d6aedac7c | ||
|
|
61ce9e70e9 | ||
|
|
ecce15c39b | ||
|
|
085fcda2ba | ||
|
|
e0abfcb4b0 | ||
|
|
7eb586f520 | ||
|
|
138e29db58 | ||
|
|
1a87feb84d | ||
|
|
5bd22410a6 | ||
|
|
16f6e3a4b2 | ||
|
|
2434c3ec7b | ||
|
|
c725881e67 | ||
|
|
51892a840c | ||
|
|
a2fa694553 | ||
|
|
4e74e33dde | ||
|
|
2d4418b36f | ||
|
|
308369452d | ||
|
|
b638fa0c73 | ||
|
|
101acdd583 | ||
|
|
ad904d6b05 | ||
|
|
b499a0e121 | ||
|
|
4543856ce1 | ||
|
|
f97db75db0 | ||
|
|
5a545fa122 | ||
|
|
8a50f80f28 | ||
|
|
6a754894d2 | ||
|
|
24b25f684a | ||
|
|
99c865e0bd | ||
|
|
b68624a961 | ||
|
|
2e84d11c53 | ||
|
|
94d94e9f46 | ||
|
|
8b504a2916 | ||
|
|
32ec824dab | ||
|
|
5e4791ea2f | ||
|
|
53d46034d5 | ||
|
|
0bf2d04885 | ||
|
|
2daf9dae80 | ||
|
|
23ad35b7d7 | ||
|
|
99a94fbb93 | ||
|
|
3b3a11f31b | ||
|
|
f981502781 | ||
|
|
40a505dbc3 | ||
|
|
9adac463e9 | ||
|
|
858710cfec | ||
|
|
7de26aee7e | ||
|
|
558a6bd724 | ||
|
|
b6a048e4f6 | ||
|
|
498c1ce2b8 | ||
|
|
2128aba603 | ||
|
|
3de50dc707 | ||
|
|
fe8c25576e | ||
|
|
da486ca841 | ||
|
|
8e1c6c7a39 | ||
|
|
e40694ff9d | ||
|
|
4638406fc9 | ||
|
|
704ee94101 | ||
|
|
6c96ad1f93 | ||
|
|
8cebbd23d8 | ||
|
|
4a4437b500 | ||
|
|
bd83df28bc | ||
|
|
010ceab910 | ||
|
|
c6f1cfdd4a | ||
|
|
e09cf2aaf8 | ||
|
|
85680bd36d | ||
|
|
36f3bf6b9c | ||
|
|
8df1c3f152 | ||
|
|
ee44dbd677 | ||
|
|
05112b559f | ||
|
|
0fa669db94 | ||
|
|
e5d44cd9c3 | ||
|
|
5d682f69a8 | ||
|
|
df3724c830 | ||
|
|
e093ca5f49 | ||
|
|
b972d1db5a | ||
|
|
015d17db3f | ||
|
|
617f2441fc | ||
|
|
c18f28d22d | ||
|
|
f550eeec28 | ||
|
|
22907e01ef | ||
|
|
59a7d99e3c | ||
|
|
f4b3a0f4ac | ||
|
|
68d9e02417 | ||
|
|
6e85871595 | ||
|
|
6853f2c910 | ||
|
|
9d94b26cc2 | ||
|
|
58cbc9790f | ||
|
|
8311bae2ab | ||
|
|
2bfb83d6f7 | ||
|
|
f1a7c6bbc1 | ||
|
|
4d993a6dd1 | ||
|
|
950470aade | ||
|
|
7f1255b5e7 | ||
|
|
6e27ab2e0a | ||
|
|
da614e7d2c | ||
|
|
858c450223 | ||
|
|
e8f5b98cb0 | ||
|
|
339da5e19e | ||
|
|
9ef7c5b955 | ||
|
|
ad8db9c567 | ||
|
|
0210dd00a0 | ||
|
|
41f62ae03b | ||
|
|
b19f8a2eb1 | ||
|
|
1a86c9f45c | ||
|
|
5968bae002 | ||
|
|
7014f9a125 | ||
|
|
7536d6b95f | ||
|
|
6036b13e94 | ||
|
|
d2e7fc1863 | ||
|
|
27b7f220a3 | ||
|
|
9e360e8dc0 | ||
|
|
a2acb7a53f | ||
|
|
d9db6b02ce | ||
|
|
ae80d921c0 | ||
|
|
14a2ca89ea | ||
|
|
b2517e3302 | ||
|
|
f83949c508 | ||
|
|
af439c6a66 | ||
|
|
71d700ea28 | ||
|
|
90b2062d85 | ||
|
|
e6a91e5f6c | ||
|
|
421d616682 | ||
|
|
fb5030010c | ||
|
|
421ad35034 | ||
|
|
a17aef5af8 | ||
|
|
6544c5cbcc | ||
|
|
a0518a0636 | ||
|
|
5e669bb8c7 | ||
|
|
29ef442e99 | ||
|
|
2ae5582a9e | ||
|
|
0ec334e252 | ||
|
|
db9cbcb457 | ||
|
|
d04f8d9029 | ||
|
|
c4d09893cd | ||
|
|
040735b1c1 | ||
|
|
d92502b212 | ||
|
|
6b00925a6a | ||
|
|
b9142ad728 | ||
|
|
b71c2e53b7 | ||
|
|
598a2186d7 | ||
|
|
fcdfb9e588 | ||
|
|
b00840e147 | ||
|
|
f2473f7f74 | ||
|
|
673d97c19b | ||
|
|
32acc62cde | ||
|
|
c568db2704 | ||
|
|
fa6c5d0537 | ||
|
|
62d9a002f7 | ||
|
|
8b59e8eb3b | ||
|
|
eec6a27cca | ||
|
|
3b68fef220 | ||
|
|
92ec78086d | ||
|
|
cf9c2908b4 | ||
|
|
765a3b2968 | ||
|
|
89c7bb356b | ||
|
|
023c08fcdb | ||
|
|
29233a3455 | ||
|
|
7b97bdd0ef | ||
|
|
21597fa2f8 | ||
|
|
336039f963 | ||
|
|
c7125967c9 | ||
|
|
3e0a3ff1ea | ||
|
|
fa95f956a0 | ||
|
|
ba83cc1006 | ||
|
|
17dd0757d3 | ||
|
|
e02a3d452c | ||
|
|
33a30c38dc | ||
|
|
9a0309ac62 | ||
|
|
31ffc9ff02 | ||
|
|
8e911d728d | ||
|
|
7a12f056e4 | ||
|
|
391e8a0867 | ||
|
|
0ca1a861e9 | ||
|
|
e3135685da | ||
|
|
6f0fd8f6ec | ||
|
|
ebcb996431 | ||
|
|
8cbf68223a | ||
|
|
39f19bf3ab | ||
|
|
e8c0dd897b | ||
|
|
66fbade194 | ||
|
|
ae2761f204 | ||
|
|
a49981b6c5 | ||
|
|
d21d1453f5 | ||
|
|
11e19466f8 | ||
|
|
d51de53804 | ||
|
|
8b5448adcd | ||
|
|
cf70d7ff13 | ||
|
|
f0dc972bf2 | ||
|
|
81276fc048 | ||
|
|
58f300a20d | ||
|
|
f530772b80 | ||
|
|
8cea6cb6b5 | ||
|
|
7cf4c7857f | ||
|
|
5fe5a0febf | ||
|
|
d4ee843a01 | ||
|
|
c48ef57a84 | ||
|
|
0c3d1d7058 | ||
|
|
eda9857200 | ||
|
|
46b96f3811 | ||
|
|
7d73588c99 | ||
|
|
40c91262e6 | ||
|
|
31f23204f3 | ||
|
|
e53b8bcc52 | ||
|
|
25d3013949 | ||
|
|
8ac46e99b7 | ||
|
|
59cc153b84 | ||
|
|
29ecae7010 | ||
|
|
409f5072af | ||
|
|
58a0b160b2 | ||
|
|
c8ba79b2d1 | ||
|
|
ef1b48c992 | ||
|
|
e9819d6e57 | ||
|
|
480a208655 | ||
|
|
179f2e709d | ||
|
|
90c09b1bf1 | ||
|
|
e7719c2458 | ||
|
|
fbce5882c6 |
@@ -1,135 +0,0 @@
|
||||
---
|
||||
name: umb-release-notes
|
||||
description: Improve a set of auto-generated GitHub release notes for an Umbraco CMS release. Cross-checks the notes against every PR carrying the release label, adds any that are missing, re-files every PR under the most appropriate category, and strips purely-internal entries. Use whenever the user asks to tidy up, improve, complete, or recategorize release notes for a given version, or mentions a release-notes text file plus a version number.
|
||||
argument-hint: <version> <path-to-generated-notes-file>
|
||||
---
|
||||
|
||||
# Umbraco CMS - Improve Release Notes
|
||||
|
||||
Takes a file of auto-generated GitHub release notes and produces an improved version that:
|
||||
|
||||
1. **Is complete** — every merged PR carrying the `release/<version>` label appears.
|
||||
2. **Is well-categorized** — every PR sits under the most appropriate heading.
|
||||
3. **Is free of noise** — purely-internal entries of no value to a reader are removed.
|
||||
|
||||
The result is written to a **new** file alongside the input, so the user can diff the two.
|
||||
|
||||
**Run autonomously.** Do NOT use `AskUserQuestion` once the required arguments (version and input file path) are available — only ask if one of them is missing from `$ARGUMENTS` and cannot be inferred (see Arguments). Beyond that, make the categorization calls yourself using the rules below; if a handful are genuinely borderline, place them anyway and note the borderline ones in your closing summary so the user can override.
|
||||
|
||||
## Arguments
|
||||
|
||||
`$ARGUMENTS` contains two values:
|
||||
|
||||
1. **Version** — e.g. `17.5.0`, `18.1.0`. The GitHub label to search is `release/<version>` (so version `17.5.0` → label `release/17.5.0`).
|
||||
2. **Input file path** — full path to the text file holding the auto-generated notes (e.g. `C:\Temp\release-17.5.0-rc.md`).
|
||||
|
||||
If either is missing, ask the user once for the missing value, then proceed.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Run `gh auth status`. If it fails, tell the user to authenticate `gh` (e.g. `gh auth login`) and stop — the skill needs the GitHub CLI to query PRs. The repo is always `umbraco/Umbraco-CMS`.
|
||||
|
||||
## Procedure
|
||||
|
||||
### 1. Read the input notes
|
||||
|
||||
Read the input file. Note its structure — it is GitHub's generated format:
|
||||
|
||||
- A leading HTML comment (`<!-- Release notes generated ... -->`).
|
||||
- A `## What's Changed` heading followed by `### <emoji> <Category>` sub-headings, each with `* <title> by @<author> in <url>` bullets.
|
||||
- A trailing `## New Contributors` section and a `**Full Changelog**: ...` line.
|
||||
|
||||
Extract the set of PR numbers already present (parse the `/pull/<number>` from each bullet). Preserve each existing bullet's **exact text** (title, author, URL) when you re-emit it — only its category placement may change.
|
||||
|
||||
### 2. Fetch every labelled PR
|
||||
|
||||
```bash
|
||||
gh pr list --repo umbraco/Umbraco-CMS --label "release/<version>" --state closed --limit 1000 \
|
||||
--json number,title,author,labels,mergedAt \
|
||||
--jq '.[] | select(.mergedAt != null) | "\(.number)\t\(.author.login)\t\([.labels[].name] | join(", "))\t\(.title)"' | sort -n
|
||||
```
|
||||
|
||||
This is the authoritative list of what the release *should* contain. Each row gives number, author, labels, title.
|
||||
|
||||
**Guard against silent truncation.** `gh pr list` caps at `--limit` without warning, so a large release could drop the overflow and the skill would still look "complete". Count the returned rows and compare against the limit:
|
||||
|
||||
```bash
|
||||
gh pr list --repo umbraco/Umbraco-CMS --label "release/<version>" --state closed --limit 1000 --json number --jq 'length'
|
||||
```
|
||||
|
||||
If this equals 1000, the limit was hit — raise `--limit` and re-fetch before continuing. Do **not** proceed on a truncated list.
|
||||
|
||||
### 3. Reconcile
|
||||
|
||||
- **Missing labelled PRs** (labelled but not in the input file): these must be **added**. Build a bullet as `* <title> by @<author> in https://github.com/umbraco/Umbraco-CMS/pull/<number>`.
|
||||
- **Author handle.** `<author>` in the template is the raw `.author.login` value — the bullet supplies the leading `@`, so do not prepend another. `gh`'s `.author.login` already returns bot accounts with the `[bot]` suffix as part of the login — Dependabot comes back as `dependabot[bot]`, not `dependabot` or `app/dependabot` (the `app/` form only appears in git committer metadata and CODEOWNERS, never in `gh`'s JSON). So the login is already in the right shape; use it verbatim (e.g. `.author.login` of `dependabot[bot]` renders as `@dependabot[bot]`, matching what GitHub's generator wrote for the existing bullets). The only thing to guard against is accidentally stripping or altering the `[bot]` suffix.
|
||||
- **PRs in the file but not labelled**: keep them. The generated notes span a commit range (see the `Full Changelog` compare link), so they legitimately include backports / earlier-version PRs that lack the current label. For any of these you need to categorize, fetch its labels with:
|
||||
|
||||
```bash
|
||||
gh pr view <number> --repo umbraco/Umbraco-CMS --json number,title,labels \
|
||||
--jq '"\(.number)\t\([.labels[].name] | join(", "))\t\(.title)"'
|
||||
```
|
||||
|
||||
Do **not** invent or alter the `New Contributors` section — carry it over verbatim. You cannot reliably recompute first-time contributors, so leave it as the generator produced it (mention this in the summary).
|
||||
|
||||
### 4. Categorize every PR
|
||||
|
||||
Use exactly these headings, in this order. Omit any heading that ends up with no entries.
|
||||
|
||||
| Heading | What goes here | Primary signal |
|
||||
|---|---|---|
|
||||
| `### 🙌 Notable Changes` | **Don't recategorize existing entries.** Label-driven — but still add any missing PR carrying this label here. | label `category/notable` |
|
||||
| `### 💥 Breaking Changes` | **Don't recategorize existing entries.** Label-driven — but still add any missing PR carrying this label here. | label `category/breaking` |
|
||||
| `### 📦 Dependencies` | Dependency bumps | label `dependencies`; or dependabot author |
|
||||
| `### 🚀 New Features` | New user- or developer-facing capability | label `type/feature` / `category/feature`; or title introduces/adds a genuinely new capability |
|
||||
| `### 🚤 Performance` | Performance improvements | label `category/performance`; or `Performance:` title prefix |
|
||||
| `### 🌈 Accessibility Improvements` | A11y improvements (labels, contrast, keyboard) | label `category/accessibility` / `accessibility`; or clear a11y intent (e.g. "improve contrast", "missing labels") |
|
||||
| `### 🐛 Bug Fixes` | Fixes to broken/incorrect behaviour | default for anything describing a fix |
|
||||
| `### 🧪 Testing` | Test additions/changes only | label `category/test-automation` / `area/test`; or `E2E`/`QA`/"acceptance tests"/"unit test coverage"/"add tests" titles |
|
||||
| `### 🛡️ Code Quality, Documentation and Refactoring` | Refactors, deprecations, API tidy-ups, XML/MD documentation, knowledge-base (`MD`) updates | label `category/refactor`; or titles about refactoring, deprecating, renaming, documenting, constants extraction, MD/CLAUDE.md content |
|
||||
| `### 🧑💻 Developer Experience` | Things that improve the experience of developers building on or contributing to Umbraco — dev tooling, build/watch ergonomics, test mocks/harnesses, backoffice dev utilities | `Developer Experience` title prefix; dev tooling; mock/harness changes |
|
||||
|
||||
**Rules:**
|
||||
|
||||
- **Notable and Breaking are off-limits for recategorization** — never move a PR that is *already in the input file* into or out of these sections; they are driven purely by their labels and the generator placed them correctly. This does **not** exempt them from completeness: a PR discovered as missing in step 3 that carries `category/notable` or `category/breaking` must still be **added** under the matching section.
|
||||
- Label signals beat title wording, except a `Performance:`/`Developer Experience:` title prefix is decisive for its section.
|
||||
- A PR with both `type/feature` and `category/refactor` whose title clearly describes a refactor (e.g. "swap relative imports", "re-export type") belongs under Code Quality, not New Features.
|
||||
- "Add ... tests"/"unit test coverage" → Testing, even if it also touches docs. If a PR adds XML documentation *and* tests, lead with where the title's emphasis lies (documentation → Code Quality; test coverage → Testing).
|
||||
- When a PR is genuinely 50/50, pick the more reader-useful heading and list it in your closing summary as borderline.
|
||||
|
||||
### 5. Remove purely-internal noise
|
||||
|
||||
Drop entries that have **no value to anyone reading release notes** — pure repository plumbing with no shipped impact. Examples:
|
||||
|
||||
- Branch/merge maintenance ("Fix main branch after merge issue").
|
||||
- CI/pipeline fixes that don't change the product.
|
||||
- Reverts of changes that never shipped in a release.
|
||||
|
||||
**Keep** anything that ships in the product or genuinely helps developers building on Umbraco — that includes documentation/MD updates, dev tooling, and test mocks (those go to Code Quality or Developer Experience, they are *not* noise). When unsure whether something is noise, keep it and flag it in the summary rather than silently dropping it. List every removal in your closing summary.
|
||||
|
||||
### 6. Write the output
|
||||
|
||||
Write to a new file in the **same folder** as the input, named by appending ` - with updates` before the extension:
|
||||
|
||||
- Input `C:\Temp\release-17.5.0-rc.md` → Output `C:\Temp\release-17.5.0-rc - with updates.md`
|
||||
|
||||
Preserve the leading HTML comment, the `## What's Changed` heading, the `## New Contributors` section, and the `**Full Changelog**` line exactly. Only the `### <category>` groupings and their bullets change.
|
||||
|
||||
### 7. Report
|
||||
|
||||
Give a concise summary:
|
||||
|
||||
- Count of PRs added (with their numbers), and which categories they landed in.
|
||||
- Notable recategorizations (PRs moved out of the catch-all Bug Fixes into Features/Performance/Testing/etc.).
|
||||
- Every entry removed, with the one-line reason.
|
||||
- Any borderline calls the user may want to override.
|
||||
- The output file path.
|
||||
|
||||
## Verification
|
||||
|
||||
Before reporting done, confirm:
|
||||
|
||||
- Every PR number from step 2 is present in the output (except any you deliberately removed in step 5 — and those must be in the removal list).
|
||||
- No PR appears under more than one heading.
|
||||
- Notable and Breaking sections are byte-for-byte unchanged from the input.
|
||||
- The header comment, New Contributors, and Full Changelog lines are intact.
|
||||
+2
-1
@@ -80,7 +80,8 @@ tools/docfx/
|
||||
/src/Umbraco.Web.UI/wwwroot/[Mm]edia/
|
||||
/src/Umbraco.Web.UI/App_Code/
|
||||
/src/Umbraco.Web.UI/App_Plugins/
|
||||
/src/Umbraco.Web.UI/[Uu]mbraco/[Dd]ata/
|
||||
/src/Umbraco.Web.UI/[Uu]mbraco/[Dd]ata/*
|
||||
!/src/Umbraco.Web.UI/[Uu]mbraco/[Dd]ata/Umbraco.Sample.sqlite.db
|
||||
/src/Umbraco.Web.UI/[Uu]mbraco/[Ll]ogs/
|
||||
/src/Umbraco.Web.UI/[Uu]mbraco/[Mm]odels/
|
||||
/src/Umbraco.Web.UI/Views/
|
||||
|
||||
@@ -46,7 +46,8 @@ Enterprise-grade CMS built on .NET 10.0. This repository contains 21 production
|
||||
- **ASP.NET Core** - Web framework
|
||||
- **Entity Framework Core** - Modern ORM
|
||||
- **OpenIddict** - OAuth 2.0/OpenID Connect authentication
|
||||
- **Swashbuckle** - OpenAPI/Swagger documentation
|
||||
- **Microsoft.AspNetCore.OpenApi** - OpenAPI document generation
|
||||
- **Swashbuckle.AspNetCore.SwaggerUI** - Swagger UI for API documentation
|
||||
- **Lucene.NET** - Full-text search via Examine
|
||||
- **ImageSharp** - Image processing
|
||||
|
||||
@@ -366,16 +367,27 @@ public interface IMyService
|
||||
|
||||
### Centralized Package Management
|
||||
|
||||
**All NuGet package versions** are centralized in `Directory.Packages.props`. Individual projects do NOT specify versions.
|
||||
**NuGet package versions** are centralized in `Directory.Packages.props`. There are two `Directory.Packages.props` files in the source tree, with multi-level merging enabled so the test file inherits from the root:
|
||||
|
||||
| File | Scope |
|
||||
|------|-------|
|
||||
| `Directory.Packages.props` (root) | Production source code packages — referenced by all `src/**` projects |
|
||||
| `tests/Directory.Packages.props` | Test-only packages (NUnit, Moq, Bogus, BenchmarkDotNet, etc.) — adds entries on top of the inherited root file |
|
||||
|
||||
When updating dependencies, decide which file the package belongs in:
|
||||
- A package used only by test projects → `tests/Directory.Packages.props`
|
||||
- A package used by any production project (or by both production and tests) → root `Directory.Packages.props`
|
||||
|
||||
```xml
|
||||
<!-- Individual projects reference WITHOUT version -->
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
|
||||
|
||||
<!-- Versions defined in Directory.Packages.props -->
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
|
||||
```
|
||||
|
||||
**Opt-out**: `src/Umbraco.Web.UI/Umbraco.Web.UI.csproj` sets `<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>` and specifies versions inline (for `Microsoft.EntityFrameworkCore.Design`, `Microsoft.Build.Tasks.Core`, `Microsoft.ICU.ICU4C.Runtime`, etc.). Update those versions directly in that csproj when bumping. Two further `Directory.Packages.props` files exist under `templates/` for the project/extension templates and have their own version sets — keep `Microsoft.AspNetCore.OpenApi` aligned between the root file and `templates/UmbracoExtension/`.
|
||||
|
||||
### Build Configuration
|
||||
|
||||
- `Directory.Build.props` - Shared properties (target framework, company, copyright)
|
||||
@@ -419,7 +431,8 @@ All APIs use **OpenIddict** (OAuth 2.0/OpenID Connect):
|
||||
APIs use `Asp.Versioning.Mvc`:
|
||||
- Management API: `/umbraco/management/api/v{version}/*`
|
||||
- Delivery API: `/umbraco/delivery/api/v{version}/*`
|
||||
- OpenAPI/Swagger docs per version
|
||||
- OpenAPI docs: `/umbraco/openapi/management.json`, `/umbraco/openapi/delivery.json`
|
||||
- Swagger UI: `/umbraco/openapi/`
|
||||
|
||||
### Updating `OpenApi.json` (Management API)
|
||||
|
||||
@@ -435,14 +448,6 @@ When a PR changes Management API controllers or models, the `OpenApi.json` file
|
||||
|
||||
The backoffice is published to npm as `@umbraco-cms/backoffice`. Runtime dependencies are provided via importmap; npm peerDependencies provide types only. For full details on dependency hoisting, version range logic, and plugin development, see `/src/Umbraco.Web.UI.Client/CLAUDE.md` → "npm Package Publishing".
|
||||
|
||||
### SQL Server 2100-parameter limit
|
||||
|
||||
Any `WHERE IN (@0, @1, ...)` built from a runtime-sized collection risks hitting SQL Server's 2100-parameter ceiling and throwing `SqlException` 8003 in production.
|
||||
|
||||
Batch with `IEnumerable<T>.InGroupsOf(Constants.Sql.MaxParameterCount)` or `Database.FetchByGroups(...)` whenever the collection size is driven by user data — not just when it currently fits. Watch for products of two scaling dimensions (documents × languages, properties × versions) and config-tunable batch sizes whose defaults are safe but ceilings aren't.
|
||||
|
||||
Full guidance, safe patterns and decision rule: see `/src/Umbraco.Infrastructure/CLAUDE.md` → "Avoiding the SQL Server 2100-parameter limit".
|
||||
|
||||
### Known Limitations
|
||||
|
||||
1. **Circular Dependencies**: Avoided via `Lazy<T>` or event notifications
|
||||
@@ -539,16 +544,6 @@ Allowed, but cheap to write and cheaper to leave behind. Keep them short and tra
|
||||
|
||||
---
|
||||
|
||||
## 9. Testing Practices
|
||||
|
||||
### Tests for a bug fix must fail before the fix
|
||||
|
||||
Verify any test you add for a bug fix actually catches the bug: either write the failing test first (TDD), or temporarily revert the production change and confirm the test fails before re-applying. A test that passes both ways proves nothing. Watch for coincidental passes — default seed/sort orders can make a buggy path produce the right answer for the test's specific inputs; construct inputs so the broken and fixed behaviours give visibly different results.
|
||||
|
||||
For integration tests that exercise caching or cache refreshers, see `tests/Umbraco.Tests.Integration/CLAUDE.md` — the harness disables caching by default, which can produce false greens.
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Essential Commands
|
||||
|
||||
@@ -40,8 +40,8 @@
|
||||
<!-- Package Validation -->
|
||||
<PropertyGroup>
|
||||
<GenerateCompatibilitySuppressionFile>false</GenerateCompatibilitySuppressionFile>
|
||||
<EnablePackageValidation>true</EnablePackageValidation>
|
||||
<PackageValidationBaselineVersion>17.0.0</PackageValidationBaselineVersion>
|
||||
<EnablePackageValidation>false</EnablePackageValidation> <!-- TODO (V18): Set to true once this version is released. -->
|
||||
<PackageValidationBaselineVersion>18.0.0</PackageValidationBaselineVersion>
|
||||
<EnableStrictModeForCompatibleFrameworksInPackage>true</EnableStrictModeForCompatibleFrameworksInPackage>
|
||||
<EnableStrictModeForCompatibleTfms>true</EnableStrictModeForCompatibleTfms>
|
||||
</PropertyGroup>
|
||||
|
||||
+41
-38
@@ -8,33 +8,37 @@
|
||||
<ItemGroup>
|
||||
<GlobalPackageReference Include="Nerdbank.GitVersioning" Version="3.9.50" />
|
||||
<GlobalPackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||
<GlobalPackageReference Include="Umbraco.Code" Version="2.4.0" />
|
||||
<!-- TODO (V18): Bump Umbraco.Code to 3.0.0 stable before release of 18.0.0 -->
|
||||
<GlobalPackageReference Include="Umbraco.Code" Version="3.0.0-beta" />
|
||||
<GlobalPackageReference Include="Umbraco.GitVersioning.Extensions" Version="0.2.0" />
|
||||
</ItemGroup>
|
||||
<!-- Microsoft packages -->
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="10.0.6" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.6" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.6" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="10.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="10.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="10.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="10.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="10.0.6" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="10.0.7" />
|
||||
<!-- When updating this version, also update templates/UmbracoExtension/Umbraco.Extension.csproj -->
|
||||
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.3.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="5.3.0" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Hybrid" Version="10.5.0" />
|
||||
<PackageVersion Include="System.Linq.Async" Version="7.0.0" />
|
||||
<PackageVersion Include="System.Linq.Async" Version="7.0.1" />
|
||||
</ItemGroup>
|
||||
<!-- Umbraco packages -->
|
||||
<ItemGroup>
|
||||
@@ -42,42 +46,41 @@
|
||||
</ItemGroup>
|
||||
<!-- Third-party packages -->
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Asp.Versioning.Mvc" Version="8.1.1" />
|
||||
<PackageVersion Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.1" />
|
||||
<PackageVersion Include="Asp.Versioning.Mvc" Version="10.0.0" />
|
||||
<PackageVersion Include="Asp.Versioning.Mvc.ApiExplorer" Version="10.0.0" />
|
||||
<PackageVersion Include="Dazinator.Extensions.FileProviders" Version="2.0.0" />
|
||||
<PackageVersion Include="Examine" Version="3.8.0" />
|
||||
<PackageVersion Include="Examine.Core" Version="3.8.0" />
|
||||
<PackageVersion Include="Examine" Version="3.7.1" />
|
||||
<PackageVersion Include="Examine.Core" Version="3.7.1" />
|
||||
<PackageVersion Include="HtmlAgilityPack" Version="1.12.4" />
|
||||
<PackageVersion Include="JsonPatch.Net" Version="3.3.0" />
|
||||
<PackageVersion Include="K4os.Compression.LZ4" Version="1.3.8" />
|
||||
<PackageVersion Include="MailKit" Version="4.16.0" />
|
||||
<PackageVersion Include="Markdig" Version="0.45.0" />
|
||||
<PackageVersion Include="Markdig" Version="1.1.3" />
|
||||
<PackageVersion Include="Markdown" Version="2.2.1" />
|
||||
<PackageVersion Include="MessagePack" Version="3.1.7" />
|
||||
<PackageVersion Include="MessagePack" Version="3.1.4" />
|
||||
<PackageVersion Include="MiniProfiler.AspNetCore.Mvc" Version="4.5.4" />
|
||||
<PackageVersion Include="MiniProfiler.Shared" Version="4.5.4" />
|
||||
<PackageVersion Include="ncrontab" Version="3.4.0" />
|
||||
<PackageVersion Include="NPoco" Version="6.2.0" />
|
||||
<PackageVersion Include="NPoco.SqlServer" Version="6.2.0" />
|
||||
<PackageVersion Include="OpenIddict.Abstractions" Version="7.4.0" />
|
||||
<PackageVersion Include="OpenIddict.AspNetCore" Version="7.4.0" />
|
||||
<PackageVersion Include="OpenIddict.EntityFrameworkCore" Version="7.4.0" />
|
||||
<PackageVersion Include="OpenIddict.Abstractions" Version="7.5.0" />
|
||||
<PackageVersion Include="OpenIddict.AspNetCore" Version="7.5.0" />
|
||||
<PackageVersion Include="OpenIddict.EntityFrameworkCore" Version="7.5.0" />
|
||||
<PackageVersion Include="Serilog" Version="4.3.1" />
|
||||
<PackageVersion Include="Serilog.AspNetCore" Version="9.0.0" />
|
||||
<PackageVersion Include="Serilog.AspNetCore" Version="10.0.0" />
|
||||
<PackageVersion Include="Serilog.Enrichers.Process" Version="3.0.0" />
|
||||
<PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" />
|
||||
<PackageVersion Include="Serilog.Expressions" Version="5.0.0" />
|
||||
<PackageVersion Include="Serilog.Extensions.Hosting" Version="9.0.0" />
|
||||
<PackageVersion Include="Serilog.Extensions.Hosting" Version="10.0.0" />
|
||||
<PackageVersion Include="Serilog.Formatting.Compact" Version="3.0.0" />
|
||||
<PackageVersion Include="Serilog.Formatting.Compact.Reader" Version="4.0.0" />
|
||||
<PackageVersion Include="Serilog.Settings.Configuration" Version="9.0.0" />
|
||||
<PackageVersion Include="Serilog.Settings.Configuration" Version="10.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Async" Version="2.1.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Map" Version="2.0.0" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.12" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp.Web" Version="3.2.0" />
|
||||
<!-- When updating this version, also update templates/UmbracoExtension/Umbraco.Extension.csproj -->
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="10.1.7" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.1.7" />
|
||||
</ItemGroup>
|
||||
<!-- Transitive pinned versions (only required because our direct dependencies have vulnerable versions of transitive dependencies) -->
|
||||
<ItemGroup>
|
||||
@@ -90,6 +93,6 @@
|
||||
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
|
||||
<!-- Examine (via Microsoft.AspNetCore.DataProtection 8.0.4) references a vulnerable version of the following: -->
|
||||
<!-- TODO: Remove this pinned dependency when Examine updates its Microsoft.AspNetCore.DataProtection reference. -->
|
||||
<PackageVersion Include="System.Security.Cryptography.Xml" Version="10.0.6" />
|
||||
<PackageVersion Include="System.Security.Cryptography.Xml" Version="10.0.7" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
+98
-32
@@ -825,31 +825,74 @@ stages:
|
||||
publishFeedCredentials: "MyGet - Umbraco Nightly"
|
||||
${{ else }}:
|
||||
publishFeedCredentials: "MyGet - Pre-releases"
|
||||
# Pre-release/nightly feeds: keep the `latest` dist-tag default (no `next` split).
|
||||
- job:
|
||||
displayName: Push to pre-release feed (npm)
|
||||
steps:
|
||||
- template: templates/npm-publish.yml
|
||||
parameters:
|
||||
artifactName: npm
|
||||
- checkout: none
|
||||
- download: current
|
||||
artifact: npm
|
||||
- bash: |
|
||||
# Check if we are on a nightly build
|
||||
if [ $isNightly = "False" ]; then
|
||||
echo "##[debug]Prerelease build detected"
|
||||
registry="https://www.myget.org/F/umbracoprereleases/npm/"
|
||||
else
|
||||
echo "##[debug]Nightly build detected"
|
||||
registry="https://www.myget.org/F/umbraconightly/npm/"
|
||||
fi
|
||||
echo "@umbraco-cms:registry=$registry" >> .npmrc
|
||||
env:
|
||||
isNightly: ${{parameters.isNightly}}
|
||||
workingDirectory: $(Pipeline.Workspace)/npm
|
||||
displayName: Add scoped registry to .npmrc
|
||||
- task: npmAuthenticate@0
|
||||
displayName: Authenticate with npm (MyGet)
|
||||
inputs:
|
||||
workingFile: "$(Pipeline.Workspace)/npm/.npmrc"
|
||||
customEndpoint: "MyGet (npm) - Umbracoprereleases, MyGet (npm) - Umbraconightly"
|
||||
displayName: Push to npm (MyGet)
|
||||
${{ if eq(parameters.isNightly, true) }}:
|
||||
registry: https://www.myget.org/F/umbraconightly/npm/
|
||||
${{ else }}:
|
||||
registry: https://www.myget.org/F/umbracoprereleases/npm/
|
||||
- bash: |
|
||||
# Setup temp npm project to load in defaults from the local .npmrc
|
||||
npm init -y
|
||||
|
||||
# Find the first .tgz file in the current directory and publish it
|
||||
files=( ./*.tgz )
|
||||
npm publish "${files[0]}"
|
||||
displayName: Push to npm (MyGet)
|
||||
workingDirectory: $(Pipeline.Workspace)/npm
|
||||
- job: PublishTestHelpersNpm
|
||||
displayName: Push TestHelpers to pre-release feed (npm)
|
||||
steps:
|
||||
- template: templates/npm-publish.yml
|
||||
parameters:
|
||||
artifactName: npm-testhelpers
|
||||
- checkout: none
|
||||
- download: current
|
||||
artifact: npm-testhelpers
|
||||
- bash: |
|
||||
# Check if we are on a nightly build
|
||||
if [ $isNightly = "False" ]; then
|
||||
echo "##[debug]Prerelease build detected"
|
||||
registry="https://www.myget.org/F/umbracoprereleases/npm/"
|
||||
else
|
||||
echo "##[debug]Nightly build detected"
|
||||
registry="https://www.myget.org/F/umbraconightly/npm/"
|
||||
fi
|
||||
echo "@umbraco-cms:registry=$registry" >> .npmrc
|
||||
env:
|
||||
isNightly: ${{parameters.isNightly}}
|
||||
workingDirectory: $(Pipeline.Workspace)/npm-testhelpers
|
||||
displayName: Add scoped registry to .npmrc
|
||||
- task: npmAuthenticate@0
|
||||
displayName: Authenticate with npm (MyGet)
|
||||
inputs:
|
||||
workingFile: "$(Pipeline.Workspace)/npm-testhelpers/.npmrc"
|
||||
customEndpoint: "MyGet (npm) - Umbracoprereleases, MyGet (npm) - Umbraconightly"
|
||||
displayName: Push test helpers to npm (MyGet)
|
||||
${{ if eq(parameters.isNightly, true) }}:
|
||||
registry: https://www.myget.org/F/umbraconightly/npm/
|
||||
${{ else }}:
|
||||
registry: https://www.myget.org/F/umbracoprereleases/npm/
|
||||
- bash: |
|
||||
# Setup temp npm project to load in defaults from the local .npmrc
|
||||
npm init -y
|
||||
|
||||
# Find the first .tgz file in the current directory and publish it
|
||||
files=( ./*.tgz )
|
||||
npm publish "${files[0]}"
|
||||
displayName: Push test helpers to npm (MyGet)
|
||||
workingDirectory: $(Pipeline.Workspace)/npm-testhelpers
|
||||
|
||||
- stage: Deploy_NuGet
|
||||
displayName: NuGet release
|
||||
@@ -898,30 +941,53 @@ stages:
|
||||
condition: and(in(dependencies.Deploy_NuGet.result, 'Succeeded', 'SucceededWithIssues'), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.npmDeploy}}))
|
||||
dependsOn:
|
||||
- Deploy_NuGet
|
||||
variables:
|
||||
# `latest` for stable releases, `next` for prereleases.
|
||||
npmDistTag: $[ iif(eq(stageDependencies.Build.A.outputs['build.NBGV_PrereleaseVersionNoLeadingHyphen'], ''), 'latest', 'next') ]
|
||||
jobs:
|
||||
- job: Publish
|
||||
displayName: Push to NPM
|
||||
steps:
|
||||
- template: templates/npm-publish.yml
|
||||
parameters:
|
||||
artifactName: npm
|
||||
registry: https://registry.npmjs.org/
|
||||
- checkout: none
|
||||
- download: current
|
||||
artifact: npm
|
||||
- bash: echo "@umbraco-cms:registry=https://registry.npmjs.org" >> .npmrc
|
||||
workingDirectory: $(Pipeline.Workspace)/npm
|
||||
displayName: Add scoped registry to .npmrc
|
||||
- task: npmAuthenticate@0
|
||||
displayName: Authenticate with npm
|
||||
inputs:
|
||||
workingFile: $(Pipeline.Workspace)/npm/.npmrc
|
||||
customEndpoint: "NPM - Umbraco Backoffice"
|
||||
displayName: Push to npm
|
||||
npmTag: $(npmDistTag)
|
||||
- script: |
|
||||
# Setup temp npm project to load in defaults from the local .npmrc
|
||||
npm init -y
|
||||
|
||||
# Find the first .tgz file in the current directory and publish it
|
||||
files=( ./*.tgz )
|
||||
npm publish "${files[0]}"
|
||||
displayName: Push to npm
|
||||
workingDirectory: $(Pipeline.Workspace)/npm
|
||||
- job: PublishTestHelpers
|
||||
displayName: Push Test Helpers to NPM
|
||||
steps:
|
||||
- template: templates/npm-publish.yml
|
||||
parameters:
|
||||
artifactName: npm-testhelpers
|
||||
registry: https://registry.npmjs.org/
|
||||
- checkout: none
|
||||
- download: current
|
||||
artifact: npm-testhelpers
|
||||
- bash: echo "@umbraco-cms:registry=https://registry.npmjs.org" >> .npmrc
|
||||
workingDirectory: $(Pipeline.Workspace)/npm-testhelpers
|
||||
displayName: Add scoped registry to .npmrc
|
||||
- task: npmAuthenticate@0
|
||||
displayName: Authenticate with npm
|
||||
inputs:
|
||||
workingFile: $(Pipeline.Workspace)/npm-testhelpers/.npmrc
|
||||
customEndpoint: "NPM - Umbraco Backoffice"
|
||||
displayName: Push test helpers to npm
|
||||
npmTag: $(npmDistTag)
|
||||
- script: |
|
||||
# Setup temp npm project to load in defaults from the local .npmrc
|
||||
npm init -y
|
||||
|
||||
# Find the first .tgz file in the current directory and publish it
|
||||
files=( ./*.tgz )
|
||||
npm publish "${files[0]}"
|
||||
displayName: Push test helpers to npm
|
||||
workingDirectory: $(Pipeline.Workspace)/npm-testhelpers
|
||||
|
||||
- stage: Upload_API_Docs
|
||||
pool:
|
||||
|
||||
@@ -4,11 +4,11 @@ pr: none
|
||||
trigger: none
|
||||
|
||||
schedules:
|
||||
- cron: '0 3 * * *'
|
||||
displayName: Daily 3AM build (v17/dev)
|
||||
- cron: '0 6 * * *'
|
||||
displayName: Daily 6AM build (v18/dev)
|
||||
branches:
|
||||
include:
|
||||
- v17/dev
|
||||
- v18/dev
|
||||
|
||||
parameters:
|
||||
- name: skipIntegrationTests
|
||||
@@ -199,31 +199,37 @@ stages:
|
||||
SA_PASSWORD: UmbracoAcceptance123!
|
||||
strategy:
|
||||
matrix:
|
||||
# We split the tests into 4 parts for each OS to reduce the time it takes to run them on the pipeline
|
||||
WindowsPart1Of4:
|
||||
# Windows is split into 5 parts (ManagementApi split in two to avoid memory pressure on LocalDb); Linux into 4.
|
||||
WindowsPart1Of5:
|
||||
vmImage: "windows-latest"
|
||||
Tests__Database__DatabaseType: LocalDb
|
||||
Tests__Database__SQLServerMasterConnectionString: N/A
|
||||
# Filter tests that are part of the Umbraco.Infrastructure namespace but not part of the Umbraco.Infrastructure.Service namespace
|
||||
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure) & (FullyQualifiedName!~Umbraco.Infrastructure.Service)"
|
||||
WindowsPart2Of4:
|
||||
WindowsPart2Of5:
|
||||
vmImage: "windows-latest"
|
||||
Tests__Database__DatabaseType: LocalDb
|
||||
Tests__Database__SQLServerMasterConnectionString: N/A
|
||||
# Filter tests that are part of the Umbraco.Infrastructure.Service namespace
|
||||
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure.Service)"
|
||||
WindowsPart3Of4:
|
||||
WindowsPart3Of5:
|
||||
vmImage: "windows-latest"
|
||||
Tests__Database__DatabaseType: LocalDb
|
||||
Tests__Database__SQLServerMasterConnectionString: N/A
|
||||
# Filter tests that are not part of the Umbraco.Infrastructure and ManagementApi namespace.
|
||||
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure) & (FullyQualifiedName!~ManagementApi)"
|
||||
WindowsPart4Of4:
|
||||
WindowsPart4Of5:
|
||||
vmImage: "windows-latest"
|
||||
Tests__Database__DatabaseType: LocalDb
|
||||
Tests__Database__SQLServerMasterConnectionString: N/A
|
||||
# Filter tests that are part of the ManagementApi namespace.
|
||||
testFilter: "(FullyQualifiedName~ManagementApi)"
|
||||
# ManagementApi, heavier sub-namespaces. Trailing dots prevent "User." from matching "UserGroup." etc.
|
||||
testFilter: "FullyQualifiedName~ManagementApi & (FullyQualifiedName~ManagementApi.Element. | FullyQualifiedName~ManagementApi.User. | FullyQualifiedName~ManagementApi.Document. | FullyQualifiedName~ManagementApi.DataType. | FullyQualifiedName~ManagementApi.DocumentType. | FullyQualifiedName~ManagementApi.MediaType. | FullyQualifiedName~ManagementApi.Template.)"
|
||||
WindowsPart5Of5:
|
||||
vmImage: "windows-latest"
|
||||
Tests__Database__DatabaseType: LocalDb
|
||||
Tests__Database__SQLServerMasterConnectionString: N/A
|
||||
# ManagementApi, remainder (complement of Part4). vstest filters do not support group
|
||||
testFilter: "FullyQualifiedName~ManagementApi & FullyQualifiedName!~ManagementApi.Element. & FullyQualifiedName!~ManagementApi.User. & FullyQualifiedName!~ManagementApi.Document. & FullyQualifiedName!~ManagementApi.DataType. & FullyQualifiedName!~ManagementApi.DocumentType. & FullyQualifiedName!~ManagementApi.MediaType. & FullyQualifiedName!~ManagementApi.Template."
|
||||
LinuxPart1Of4:
|
||||
vmImage: "ubuntu-latest"
|
||||
Tests__Database__DatabaseType: SqlServer
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
parameters:
|
||||
- name: artifactName # "npm" or "npm-testhelpers"
|
||||
type: string
|
||||
- name: registry # scoped-registry URL to publish to
|
||||
type: string
|
||||
- name: customEndpoint # npmAuthenticate service connection(s)
|
||||
type: string
|
||||
- name: displayName # label for the publish step
|
||||
type: string
|
||||
- name: npmTag # dist-tag to publish under
|
||||
type: string
|
||||
default: latest
|
||||
|
||||
steps:
|
||||
- checkout: none
|
||||
- download: current
|
||||
artifact: ${{ parameters.artifactName }}
|
||||
- script: npm config set @umbraco-cms:registry ${{ parameters.registry }} --location=project
|
||||
displayName: Add scoped registry to .npmrc
|
||||
workingDirectory: $(Pipeline.Workspace)/${{ parameters.artifactName }}
|
||||
- task: npmAuthenticate@0
|
||||
displayName: Authenticate with npm
|
||||
inputs:
|
||||
workingFile: $(Pipeline.Workspace)/${{ parameters.artifactName }}/.npmrc
|
||||
customEndpoint: ${{ parameters.customEndpoint }}
|
||||
- script: npm publish *.tgz --tag ${{ parameters.npmTag }}
|
||||
displayName: ${{ parameters.displayName }}
|
||||
workingDirectory: $(Pipeline.Workspace)/${{ parameters.artifactName }}
|
||||
@@ -13,7 +13,8 @@ Shared infrastructure for Umbraco CMS REST APIs (Management and Delivery).
|
||||
### Key Technologies
|
||||
|
||||
- **ASP.NET Core** - Web framework
|
||||
- **Swashbuckle** - OpenAPI/Swagger documentation generation
|
||||
- **Microsoft.AspNetCore.OpenApi** - OpenAPI document generation
|
||||
- **Swashbuckle.AspNetCore.SwaggerUI** - Swagger UI for browsing API documentation
|
||||
- **OpenIddict** - OAuth 2.0/OpenID Connect authentication
|
||||
- **Asp.Versioning** - API versioning
|
||||
- **System.Text.Json** - Polymorphic JSON serialization
|
||||
@@ -27,14 +28,18 @@ Shared infrastructure for Umbraco CMS REST APIs (Management and Delivery).
|
||||
|
||||
```
|
||||
Umbraco.Cms.Api.Common/
|
||||
├── OpenApi/ # Schema/Operation ID handlers for Swagger
|
||||
│ ├── SchemaIdHandler.cs # Generates schema IDs (e.g., "PagedUserModel")
|
||||
│ ├── OperationIdHandler.cs # Generates operation IDs
|
||||
│ └── SubTypesHandler.cs # Polymorphism support
|
||||
├── OpenApi/ # OpenAPI transformers and schema generators
|
||||
│ ├── UmbracoSchemaIdGenerator.cs # Generates schema IDs (e.g., "PagedUserModel")
|
||||
│ ├── UmbracoOperationIdTransformer.cs # Generates operation IDs
|
||||
│ ├── SortTagsAndPathsTransformer.cs # Sorts OpenAPI tags and paths
|
||||
│ ├── TagActionsByGroupNameTransformer.cs # Tags operations by controller group
|
||||
│ ├── FixFileReturnTypesTransformer.cs # Fixes file return type schemas
|
||||
│ ├── RequireNonNullablePropertiesSchemaTransformer.cs # Schema nullability
|
||||
│ └── OpenApiRouteTemplatePipelineFilter.cs # Adds OpenAPI endpoints
|
||||
├── Serialization/ # JSON type resolution
|
||||
│ └── UmbracoJsonTypeInfoResolver.cs
|
||||
├── Configuration/ # Options configuration
|
||||
│ ├── ConfigureUmbracoSwaggerGenOptions.cs
|
||||
│ ├── ConfigureUmbracoOpenApiOptionsBase.cs
|
||||
│ └── ConfigureOpenIddict.cs
|
||||
├── DependencyInjection/ # Service registration
|
||||
│ ├── UmbracoBuilderApiExtensions.cs
|
||||
@@ -47,9 +52,8 @@ Umbraco.Cms.Api.Common/
|
||||
|
||||
### Design Patterns
|
||||
|
||||
1. **Strategy Pattern** - `ISchemaIdHandler`, `IOperationIdHandler` (extensible via inheritance)
|
||||
2. **Builder Pattern** - `ProblemDetailsBuilder` for fluent error responses
|
||||
3. **Options Pattern** - All configuration via `IConfigureOptions<T>`
|
||||
1. **Builder Pattern** - `ProblemDetailsBuilder` for fluent error responses
|
||||
2. **Options Pattern** - All configuration via `IConfigureOptions<T>`
|
||||
|
||||
---
|
||||
|
||||
@@ -61,25 +65,12 @@ See "Quick Reference" section at bottom for common commands.
|
||||
|
||||
## 3. Key Patterns
|
||||
|
||||
### Virtual Handlers for Extensibility
|
||||
### Schema ID Generation (OpenApi/UmbracoSchemaIdGenerator.cs)
|
||||
|
||||
Handlers are intentionally virtual to allow consuming APIs to override:
|
||||
Static utility class that generates OpenAPI schema IDs following Umbraco's naming conventions:
|
||||
|
||||
```csharp
|
||||
// NOTE: Left unsealed on purpose, so it is extendable.
|
||||
public class SchemaIdHandler : ISchemaIdHandler
|
||||
{
|
||||
public virtual bool CanHandle(Type type) { }
|
||||
public virtual string Handle(Type type) { }
|
||||
}
|
||||
```
|
||||
|
||||
**Why**: Management and Delivery APIs can customize schema/operation ID generation.
|
||||
|
||||
### Schema ID Sanitization (OpenApi/SchemaIdHandler.cs:24-29, 32)
|
||||
|
||||
```csharp
|
||||
// Add "Model" suffix to avoid TypeScript name clashes (lines 24-29)
|
||||
// Add "Model" suffix to avoid TypeScript name clashes
|
||||
if (name.EndsWith("Model") == false)
|
||||
{
|
||||
// because some models names clash with common classes in TypeScript (i.e. Document),
|
||||
@@ -87,10 +78,12 @@ if (name.EndsWith("Model") == false)
|
||||
name = $"{name}Model";
|
||||
}
|
||||
|
||||
// Remove invalid characters to prevent OpenAPI generation errors (line 32)
|
||||
// Remove invalid characters to prevent OpenAPI generation errors
|
||||
return Regex.Replace(name, @"[^\w]", string.Empty);
|
||||
```
|
||||
|
||||
**Generic Type Handling**: `PagedViewModel<RelationItemViewModel>` becomes `PagedRelationItemModel`
|
||||
|
||||
### Polymorphic Deserialization (Serialization/UmbracoJsonTypeInfoResolver.cs:29-35)
|
||||
|
||||
```csharp
|
||||
@@ -116,9 +109,12 @@ if (type.IsInterface is false)
|
||||
dotnet test tests/Umbraco.Tests.Integration/
|
||||
|
||||
# Verify OpenAPI generation
|
||||
# 1. Run Management API
|
||||
# 2. Navigate to /umbraco/swagger/
|
||||
# 1. Run the application: dotnet run --project src/Umbraco.Web.UI
|
||||
# 2. Navigate to /umbraco/openapi/ for Swagger UI
|
||||
# 3. Check schema IDs and operation IDs
|
||||
# OpenAPI JSON documents available at:
|
||||
# - /umbraco/openapi/management.json (Management API)
|
||||
# - /umbraco/openapi/delivery.json (Delivery API)
|
||||
```
|
||||
|
||||
**Focus areas when testing**:
|
||||
@@ -207,49 +203,24 @@ catch (NotSupportedException exception)
|
||||
|
||||
**Issue**: Type names like `Document` clash with TypeScript built-ins.
|
||||
|
||||
**Solution**: Add "Model" suffix (OpenApi/SchemaIdHandler.cs:24-29)
|
||||
**Solution**: `UmbracoSchemaIdGenerator` adds "Model" suffix to all schema names.
|
||||
|
||||
### Generic Type Handling
|
||||
|
||||
**Issue**: `PagedViewModel<T>` needs flattened schema name.
|
||||
|
||||
**Solution** (OpenApi/SchemaIdHandler.cs:41-50):
|
||||
```csharp
|
||||
private string HandleGenerics(string name, Type type)
|
||||
{
|
||||
if (!type.IsGenericType)
|
||||
return name;
|
||||
|
||||
// use attribute custom name or append the generic type names
|
||||
// turns "PagedViewModel<RelationItemViewModel>" into "PagedRelationItem"
|
||||
return $"{name}{string.Join(string.Empty, type.GenericTypeArguments.Select(SanitizedTypeName))}";
|
||||
}
|
||||
```
|
||||
**Solution**: `UmbracoSchemaIdGenerator.Generate()` flattens generic types:
|
||||
- `PagedViewModel<RelationItemViewModel>` becomes `PagedRelationItemModel`
|
||||
|
||||
---
|
||||
|
||||
## 7. Extending This Library
|
||||
|
||||
### Adding a Custom OpenAPI Handler
|
||||
### Adding Custom OpenAPI Transformers
|
||||
|
||||
1. **Implement interface**:
|
||||
```csharp
|
||||
public class MySchemaIdHandler : SchemaIdHandler
|
||||
{
|
||||
public override bool CanHandle(Type type)
|
||||
=> type.Namespace?.StartsWith("MyProject") is true;
|
||||
OpenAPI transformers are scoped per-document. To customize a document, implement `IOpenApiDocumentTransformer`, `IOpenApiOperationTransformer`, or `IOpenApiSchemaTransformer` and register with your OpenAPI options.
|
||||
|
||||
public override string Handle(Type type)
|
||||
=> $"My{base.Handle(type)}";
|
||||
}
|
||||
```
|
||||
|
||||
2. **Register in consuming API**:
|
||||
```csharp
|
||||
builder.Services.AddSingleton<ISchemaIdHandler, MySchemaIdHandler>();
|
||||
```
|
||||
|
||||
**Note**: Handlers registered later take precedence in the selector.
|
||||
For schema ID generation, use the static `UmbracoSchemaIdGenerator.Generate(Type)` method.
|
||||
|
||||
### Customizing Problem Details
|
||||
|
||||
@@ -269,13 +240,9 @@ return BadRequest(problemDetails);
|
||||
|
||||
## 8. Project-Specific Notes
|
||||
|
||||
### Why Virtual Handlers?
|
||||
### Per-Document Transformer Scoping
|
||||
|
||||
**Decision**: Make `SchemaIdHandler`, `OperationIdHandler`, etc. virtual.
|
||||
|
||||
**Why**: Management API and Delivery API have different schema ID requirements. Virtual methods allow override without rewriting the entire handler.
|
||||
|
||||
**Example**: Management API might prefix all schemas with "Management", Delivery API with "Delivery".
|
||||
With Microsoft.AspNetCore.OpenApi, transformers are configured per OpenAPI document. This means custom transformers only apply to the documents they're registered with, not globally. Each API (Management, Delivery) configures its own transformers via `ConfigureUmbracoOpenApiOptionsBase` subclasses.
|
||||
|
||||
### Performance: Subtype Caching
|
||||
|
||||
@@ -304,9 +271,13 @@ return BadRequest(problemDetails);
|
||||
- Version: See `Directory.Packages.props`
|
||||
- Uses ASP.NET Core Data Protection for token encryption
|
||||
|
||||
**Swashbuckle**:
|
||||
- OpenAPI 3.0 document generation
|
||||
- Custom filters: `EnumSchemaFilter`, `MimeTypeDocumentFilter`, `RemoveSecuritySchemesDocumentFilter`
|
||||
**Microsoft.AspNetCore.OpenApi**:
|
||||
- OpenAPI 3.1.1 document generation
|
||||
- Custom transformers: `SchemaIdTransformer`, `OperationIdTransformer`, `MimeTypeDocumentTransformer`, `ServerTransformer`
|
||||
|
||||
**Swashbuckle.AspNetCore.SwaggerUI**:
|
||||
- Swagger UI for browsing and testing API endpoints
|
||||
- Accessed at `/umbraco/openapi/`
|
||||
|
||||
**Asp.Versioning**:
|
||||
- API versioning via `ApiVersion` attribute
|
||||
@@ -318,7 +289,7 @@ return BadRequest(problemDetails);
|
||||
|
||||
### Usage Pattern
|
||||
|
||||
Consuming APIs call `builder.AddUmbracoApiOpenApiUI().AddUmbracoOpenIddict()`
|
||||
Consuming APIs call `builder.AddUmbracoOpenApi().AddUmbracoOpenIddict()`
|
||||
|
||||
---
|
||||
|
||||
@@ -346,7 +317,8 @@ dotnet list src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj package --v
|
||||
| Class | Purpose | File |
|
||||
|-------|---------|------|
|
||||
| `ProblemDetailsBuilder` | Build RFC 7807 error responses | Builders/ProblemDetailsBuilder.cs |
|
||||
| `SchemaIdHandler` | Generate OpenAPI schema IDs | OpenApi/SchemaIdHandler.cs |
|
||||
| `UmbracoSchemaIdGenerator` | Generate OpenAPI schema IDs | OpenApi/UmbracoSchemaIdGenerator.cs |
|
||||
| `UmbracoOperationIdTransformer` | Generate operation IDs | OpenApi/UmbracoOperationIdTransformer.cs |
|
||||
| `UmbracoJsonTypeInfoResolver` | Polymorphic JSON serialization | Serialization/UmbracoJsonTypeInfoResolver.cs |
|
||||
| `UmbracoBuilderAuthExtensions` | Configure OpenIddict | DependencyInjection/UmbracoBuilderAuthExtensions.cs |
|
||||
| `HideBackOfficeTokensHandler` | Secure cookie-based token storage | DependencyInjection/HideBackOfficeTokensHandler.cs |
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
using System.Reflection;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Configures the OpenAPI options for the Default API.
|
||||
/// </summary>
|
||||
internal class ConfigureDefaultApiOptions : ConfigureUmbracoOpenApiOptionsBase
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override string ApiName => DefaultApiConfiguration.ApiName;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string ApiTitle => "Default API";
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string ApiVersion => "Latest";
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string ApiDescription => "All endpoints not defined under specific APIs";
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override bool ShouldInclude(ApiDescription apiDescription)
|
||||
{
|
||||
// Exclude controllers with ExcludeFromDefaultOpenApiDocumentAttribute
|
||||
if (apiDescription.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor
|
||||
&& controllerActionDescriptor.ControllerTypeInfo.GetCustomAttribute<ExcludeFromDefaultOpenApiDocumentAttribute>() is not null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Include if explicitly mapped to this document
|
||||
if (base.ShouldInclude(apiDescription))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Include endpoints not explicitly assigned to another document
|
||||
ApiVersionMetadata apiVersionMetadata = apiDescription.ActionDescriptor.ApiVersionMetadata;
|
||||
return string.IsNullOrEmpty(apiVersionMetadata.Name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.AspNetCore.OpenApi;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.OpenApi;
|
||||
using Umbraco.Cms.Api.Common.OpenApi;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for configuring OpenAPI options for Umbraco APIs.
|
||||
/// </summary>
|
||||
internal abstract class ConfigureUmbracoOpenApiOptionsBase : IConfigureNamedOptions<OpenApiOptions>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the name/identifier of the API to configure.
|
||||
/// </summary>
|
||||
protected abstract string ApiName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name/identifier of the API to configure.
|
||||
/// </summary>
|
||||
protected abstract string ApiTitle { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the version of the API to configure.
|
||||
/// </summary>
|
||||
protected abstract string ApiVersion { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the description of the API to configure.
|
||||
/// </summary>
|
||||
protected abstract string ApiDescription { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Configure(OpenApiOptions options) => Configure(Options.DefaultName, options);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Configure(string? name, OpenApiOptions options)
|
||||
{
|
||||
if (name != ApiName)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ConfigureOpenApi(options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configure the OpenAPI options for the specified API.
|
||||
/// </summary>
|
||||
/// <param name="options">The <see cref="OpenApiOptions"/> instance to configure.</param>
|
||||
protected virtual void ConfigureOpenApi(OpenApiOptions options)
|
||||
{
|
||||
options.AddDocumentTransformer((document, _, _) =>
|
||||
{
|
||||
document.Info = new OpenApiInfo
|
||||
{
|
||||
Title = ApiTitle,
|
||||
Version = ApiVersion,
|
||||
Description = ApiDescription,
|
||||
};
|
||||
document.Servers?.Clear();
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
options.ShouldInclude = ShouldInclude;
|
||||
options.CreateSchemaReferenceId = CreateSchemaReferenceId;
|
||||
|
||||
options.AddOperationTransformer<UmbracoOperationIdTransformer>();
|
||||
|
||||
// Tag actions by group name and cleanup unused tags (caused by the tag changes)
|
||||
options
|
||||
.AddOperationTransformer<TagActionsByGroupNameTransformer>()
|
||||
.AddDocumentTransformer<TagActionsByGroupNameTransformer>()
|
||||
.AddDocumentTransformer<SortTagsAndPathsTransformer>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a schema reference ID for the given JSON type info.
|
||||
/// Returns null for types that should be inlined, the default schema ID for non-Umbraco types,
|
||||
/// or a generated schema ID for Umbraco types.
|
||||
/// </summary>
|
||||
/// <param name="jsonTypeInfo">The JSON type info to create a schema reference ID for.</param>
|
||||
/// <returns>The schema reference ID, or null if the type should be inlined.</returns>
|
||||
internal static string? CreateSchemaReferenceId(JsonTypeInfo jsonTypeInfo)
|
||||
{
|
||||
// Ensure that only types that would normally be included in the schema generation are given a schema reference ID.
|
||||
// Otherwise, we should return null to inline them.
|
||||
var defaultSchemaReferenceId = OpenApiOptions.CreateDefaultSchemaReferenceId(jsonTypeInfo);
|
||||
if (defaultSchemaReferenceId is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Type targetType = Nullable.GetUnderlyingType(jsonTypeInfo.Type) ?? jsonTypeInfo.Type;
|
||||
|
||||
if (targetType.Namespace?.StartsWith("Umbraco.Cms") is not true)
|
||||
{
|
||||
return defaultSchemaReferenceId;
|
||||
}
|
||||
|
||||
return UmbracoSchemaIdGenerator.Generate(targetType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified API description should be included in this OpenAPI document.
|
||||
/// </summary>
|
||||
/// <param name="apiDescription">The API description to evaluate.</param>
|
||||
/// <returns><c>true</c> if the endpoint should be included; otherwise, <c>false</c>.</returns>
|
||||
protected virtual bool ShouldInclude(ApiDescription apiDescription)
|
||||
{
|
||||
if (apiDescription.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor
|
||||
&& controllerActionDescriptor.HasMapToApiAttribute(ApiName))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
ApiVersionMetadata apiVersionMetadata = apiDescription.ActionDescriptor.ApiVersionMetadata;
|
||||
return apiVersionMetadata.Name == ApiName;
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.OpenApi;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using Umbraco.Cms.Api.Common.OpenApi;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Configures Swagger/OpenAPI generation options for Umbraco APIs.
|
||||
/// </summary>
|
||||
public class ConfigureUmbracoSwaggerGenOptions : IConfigureOptions<SwaggerGenOptions>
|
||||
{
|
||||
private readonly IOperationIdSelector _operationIdSelector;
|
||||
private readonly ISchemaIdSelector _schemaIdSelector;
|
||||
private readonly ISubTypesSelector _subTypesSelector;
|
||||
private readonly IDocumentInclusionSelector _documentInclusionSelector;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConfigureUmbracoSwaggerGenOptions"/> class.
|
||||
/// </summary>
|
||||
/// <param name="operationIdSelector">The operation ID selector.</param>
|
||||
/// <param name="schemaIdSelector">The schema ID selector.</param>
|
||||
/// <param name="subTypesSelector">The sub-types selector for polymorphism support.</param>
|
||||
/// <param name="documentInclusionSelector">The document inclusion selector.</param>
|
||||
public ConfigureUmbracoSwaggerGenOptions(
|
||||
IOperationIdSelector operationIdSelector,
|
||||
ISchemaIdSelector schemaIdSelector,
|
||||
ISubTypesSelector subTypesSelector,
|
||||
IDocumentInclusionSelector documentInclusionSelector)
|
||||
{
|
||||
_operationIdSelector = operationIdSelector;
|
||||
_schemaIdSelector = schemaIdSelector;
|
||||
_subTypesSelector = subTypesSelector;
|
||||
_documentInclusionSelector = documentInclusionSelector;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConfigureUmbracoSwaggerGenOptions"/> class.
|
||||
/// </summary>
|
||||
/// <param name="operationIdSelector">The operation ID selector.</param>
|
||||
/// <param name="schemaIdSelector">The schema ID selector.</param>
|
||||
/// <param name="subTypesSelector">The sub-types selector for polymorphism support.</param>
|
||||
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 19.")]
|
||||
public ConfigureUmbracoSwaggerGenOptions(
|
||||
IOperationIdSelector operationIdSelector,
|
||||
ISchemaIdSelector schemaIdSelector,
|
||||
ISubTypesSelector subTypesSelector)
|
||||
: this(
|
||||
operationIdSelector,
|
||||
schemaIdSelector,
|
||||
subTypesSelector,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IDocumentInclusionSelector>())
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Configure(SwaggerGenOptions swaggerGenOptions)
|
||||
{
|
||||
swaggerGenOptions.SwaggerDoc(
|
||||
DefaultApiConfiguration.ApiName,
|
||||
new OpenApiInfo
|
||||
{
|
||||
Title = "Default API",
|
||||
Version = "Latest",
|
||||
Description = "All endpoints not defined under specific APIs",
|
||||
});
|
||||
|
||||
swaggerGenOptions.CustomOperationIds(description => _operationIdSelector.OperationId(description));
|
||||
swaggerGenOptions.DocInclusionPredicate(_documentInclusionSelector.Include);
|
||||
swaggerGenOptions.TagActionsBy(api =>
|
||||
api.GroupName is null
|
||||
? []
|
||||
: new[] { api.GroupName });
|
||||
swaggerGenOptions.OrderActionsBy(ActionOrderBy);
|
||||
swaggerGenOptions.SchemaFilter<EnumSchemaFilter>();
|
||||
swaggerGenOptions.CustomSchemaIds(_schemaIdSelector.SchemaId);
|
||||
swaggerGenOptions.SelectSubTypesUsing(_subTypesSelector.SubTypes);
|
||||
swaggerGenOptions.SupportNonNullableReferenceTypes();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a sort key for API actions.
|
||||
/// </summary>
|
||||
/// <param name="apiDesc">The API description.</param>
|
||||
/// <returns>A string used to sort API operations in the documentation.</returns>
|
||||
/// <remarks>
|
||||
/// See https://github.com/domaindrivendev/Swashbuckle.AspNetCore#change-operation-sort-order-eg-for-ui-sorting.
|
||||
/// </remarks>
|
||||
private static string ActionOrderBy(ApiDescription apiDesc)
|
||||
=> $"{apiDesc.GroupName}_{apiDesc.ActionDescriptor.AttributeRouteInfo?.Template ?? apiDesc.ActionDescriptor.RouteValues["controller"]}_{(apiDesc.ActionDescriptor.RouteValues.TryGetValue("action", out var action) ? action : null)}_{apiDesc.HttpMethod}";
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using Microsoft.AspNetCore.Http.Json;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for replacing the internal Microsoft.AspNetCore.OpenApi schema service registration.
|
||||
/// </summary>
|
||||
internal static class OpenApiSchemaServiceExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// The full name of the internal Microsoft type whose registration is replaced.
|
||||
/// Used for a stringly-typed <see cref="ServiceDescriptor"/> lookup because the type is not publicly accessible.
|
||||
/// </summary>
|
||||
internal const string OpenApiSchemaServiceFullName = "Microsoft.AspNetCore.OpenApi.OpenApiSchemaService";
|
||||
|
||||
/// <summary>
|
||||
/// Replaces the internal Microsoft <c>OpenApiSchemaService</c> registration for the specified document so that schema
|
||||
/// generation uses the named <see cref="JsonOptions"/> rather than the default HTTP JSON options.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <param name="documentName">The OpenAPI document key (matches the keyed singleton registered by <c>AddOpenApi(documentName)</c>).</param>
|
||||
/// <param name="jsonOptionsName">The named <see cref="JsonOptions"/> to use during schema generation for this document.</param>
|
||||
/// <returns>The same <see cref="IServiceCollection"/> for chaining.</returns>
|
||||
/// <remarks>
|
||||
/// Workaround for <see href="https://github.com/dotnet/aspnetcore/issues/66340">dotnet/aspnetcore#66340</see>.
|
||||
/// </remarks>
|
||||
public static IServiceCollection ReplaceOpenApiSchemaService(
|
||||
this IServiceCollection services,
|
||||
string documentName,
|
||||
string jsonOptionsName)
|
||||
{
|
||||
ServiceDescriptor descriptor = services.FirstOrDefault(sd =>
|
||||
sd.ServiceType.FullName == OpenApiSchemaServiceFullName
|
||||
&& Equals(sd.ServiceKey, documentName))
|
||||
?? throw new InvalidOperationException(
|
||||
$"Could not find a registration for {OpenApiSchemaServiceFullName} keyed with '{documentName}'. "
|
||||
+ $"Ensure AddOpenApi(\"{documentName}\") has been called before {nameof(ReplaceOpenApiSchemaService)}, "
|
||||
+ "or check whether the internal Microsoft.AspNetCore.OpenApi registration shape has changed.");
|
||||
|
||||
services.Remove(descriptor);
|
||||
services.AddKeyedSingleton(
|
||||
descriptor.ServiceType,
|
||||
documentName,
|
||||
(sp, key) => ActivatorUtilities.CreateInstance(
|
||||
sp,
|
||||
descriptor.ServiceType,
|
||||
key,
|
||||
Options.Create(sp.GetRequiredService<IOptionsMonitor<JsonOptions>>().Get(jsonOptionsName))));
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Swashbuckle.AspNetCore.SwaggerUI;
|
||||
using Umbraco.Cms.Api.Common.OpenApi;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="IServiceCollection"/> to configure OpenAPI services.
|
||||
/// </summary>
|
||||
public static class OpenApiServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds an OpenAPI document to the OpenAPI UI document selector dropdown.
|
||||
/// </summary>
|
||||
/// <param name="services">The <see cref="IServiceCollection"/> instance.</param>
|
||||
/// <param name="documentName">The name/identifier of the OpenAPI document.</param>
|
||||
/// <param name="documentTitle">The title to display in the UI dropdown. Defaults to <paramref name="documentName"/> if not specified.</param>
|
||||
/// <returns>The <see cref="IServiceCollection"/> instance.</returns>
|
||||
public static IServiceCollection AddOpenApiDocumentToUi(
|
||||
this IServiceCollection services,
|
||||
string documentName,
|
||||
string? documentTitle = null)
|
||||
{
|
||||
services.AddOptions<SwaggerUIOptions>()
|
||||
.Configure<IOptions<UmbracoOpenApiOptions>>((swaggerUiOptions, openApiOptions) =>
|
||||
{
|
||||
var openApiRoute = openApiOptions.Value.RouteTemplate.Replace("{documentName}", documentName).EnsureStartsWith("/");
|
||||
swaggerUiOptions.SwaggerEndpoint(openApiRoute, documentTitle ?? documentName);
|
||||
swaggerUiOptions.ConfigObject.Urls = swaggerUiOptions.ConfigObject.Urls.OrderBy(x => x.Name);
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,14 @@
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Umbraco.Cms.Api.Common.Configuration;
|
||||
using Umbraco.Cms.Api.Common.OpenApi;
|
||||
using Umbraco.Cms.Api.Common.Serialization;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Hosting;
|
||||
using Umbraco.Cms.Web.Common.ApplicationBuilder;
|
||||
using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.DependencyInjection;
|
||||
|
||||
@@ -16,26 +21,51 @@ public static class UmbracoBuilderApiExtensions
|
||||
/// Adds Umbraco API OpenAPI/Swagger UI services to the builder.
|
||||
/// </summary>
|
||||
/// <param name="builder">The Umbraco builder.</param>
|
||||
/// <returns>The Umbraco builder for method chaining.</returns>
|
||||
public static IUmbracoBuilder AddUmbracoApiOpenApiUI(this IUmbracoBuilder builder)
|
||||
internal static void AddUmbracoOpenApi(this IUmbracoBuilder builder)
|
||||
{
|
||||
if (builder.Services.Any(x => !x.IsKeyedService && x.ImplementationType == typeof(OperationIdSelector)))
|
||||
if (builder.Services.Any(x => !x.IsKeyedService && x.ImplementationType == typeof(UmbracoJsonTypeInfoResolver)))
|
||||
{
|
||||
return builder;
|
||||
return;
|
||||
}
|
||||
|
||||
builder.Services.AddSwaggerGen();
|
||||
builder.Services.ConfigureOptions<ConfigureUmbracoSwaggerGenOptions>();
|
||||
builder.Services.AddOptions<UmbracoOpenApiOptions>()
|
||||
.Configure<IHostingEnvironment, IWebHostEnvironment>((options, hostingEnv, webHostEnv) =>
|
||||
{
|
||||
options.Enabled = webHostEnv.IsProduction() is false;
|
||||
var backOfficePath = hostingEnv.GetBackOfficePath().TrimStart(Constants.CharArrays.ForwardSlash);
|
||||
options.RouteTemplate = $"{backOfficePath}/openapi/{{documentName}}.json";
|
||||
options.UiRoutePrefix = $"{backOfficePath}/openapi";
|
||||
});
|
||||
builder.AddUmbracoOpenApiDocument<ConfigureDefaultApiOptions>(DefaultApiConfiguration.ApiName, "Default API");
|
||||
builder.Services.AddSingleton<IUmbracoJsonTypeInfoResolver, UmbracoJsonTypeInfoResolver>();
|
||||
builder.Services.AddSingleton<IOperationIdSelector, OperationIdSelector>();
|
||||
builder.Services.AddSingleton<IOperationIdHandler, OperationIdHandler>();
|
||||
builder.Services.AddSingleton<ISchemaIdSelector, SchemaIdSelector>();
|
||||
builder.Services.AddSingleton<ISchemaIdHandler, SchemaIdHandler>();
|
||||
builder.Services.AddSingleton<ISubTypesSelector, SubTypesSelector>();
|
||||
builder.Services.AddSingleton<ISubTypesHandler, SubTypesHandler>();
|
||||
builder.Services.AddSingleton<IDocumentInclusionSelector, DocumentInclusionSelector>();
|
||||
builder.Services.Configure<UmbracoPipelineOptions>(options => options.AddFilter(new SwaggerRouteTemplatePipelineFilter("UmbracoApiCommon")));
|
||||
builder.Services.Configure<UmbracoPipelineOptions>(options => options.AddFilter(new OpenApiRouteTemplatePipelineFilter("UmbracoApiCommon")));
|
||||
}
|
||||
|
||||
return builder;
|
||||
/// <summary>
|
||||
/// Adds and configures an Umbraco OpenAPI document with shared transformers.
|
||||
/// </summary>
|
||||
/// <param name="builder">The Umbraco builder.</param>
|
||||
/// <param name="apiName">The name/identifier of the API.</param>
|
||||
/// <param name="apiTitle">The title of the API.</param>
|
||||
/// <param name="jsonOptionsName">
|
||||
/// Optional named <c>JsonOptions</c> to use for schema generation instead of the default HTTP JSON options.
|
||||
/// When specified, replaces the internal <c>OpenApiSchemaService</c> registration for this document.
|
||||
/// </param>
|
||||
/// <typeparam name="TConfigureOptions">The type used to configure the OpenAPI options.</typeparam>
|
||||
internal static void AddUmbracoOpenApiDocument<TConfigureOptions>(
|
||||
this IUmbracoBuilder builder,
|
||||
string apiName,
|
||||
string apiTitle,
|
||||
string? jsonOptionsName = null)
|
||||
where TConfigureOptions : ConfigureUmbracoOpenApiOptionsBase
|
||||
{
|
||||
builder.Services.AddOpenApi(apiName);
|
||||
builder.Services.ConfigureOptions<TConfigureOptions>();
|
||||
builder.Services.AddOpenApiDocumentToUi(apiName, apiTitle);
|
||||
|
||||
if (jsonOptionsName is not null)
|
||||
{
|
||||
builder.Services.ReplaceOpenApiSchemaService(apiName, jsonOptionsName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Umbraco.Cms.Api.Common.Configuration;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether an API description should be included in a specific documentation set based on the document name
|
||||
/// and API metadata.
|
||||
/// </summary>
|
||||
public class DocumentInclusionSelector : IDocumentInclusionSelector
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public bool Include(string documentName, ApiDescription apiDescription)
|
||||
{
|
||||
if (apiDescription.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor
|
||||
&& controllerActionDescriptor.HasMapToApiAttribute(documentName))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
ApiVersionMetadata apiVersionMetadata = apiDescription.ActionDescriptor.GetApiVersionMetadata();
|
||||
return apiVersionMetadata.Name == documentName
|
||||
|| (string.IsNullOrEmpty(apiVersionMetadata.Name) && documentName == DefaultApiConfiguration.ApiName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.OpenApi;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// A schema filter that converts enum schemas to string type with enum member names.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This filter ensures enums are represented as strings in the OpenAPI schema,
|
||||
/// using <see cref="EnumMemberAttribute"/> values when available.
|
||||
/// </remarks>
|
||||
public class EnumSchemaFilter : ISchemaFilter
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public void Apply(IOpenApiSchema model, SchemaFilterContext context)
|
||||
{
|
||||
if (model is not OpenApiSchema schema || context.Type.IsEnum is false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
schema.Type = JsonSchemaType.String;
|
||||
schema.Format = null;
|
||||
schema.Enum = new List<JsonNode>();
|
||||
foreach (var name in Enum.GetNames(context.Type))
|
||||
{
|
||||
var actualName = context.Type.GetField(name)?.GetCustomAttribute<EnumMemberAttribute>()?.Value ?? name;
|
||||
schema.Enum.Add(actualName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// Excludes the controller from the default OpenAPI document.
|
||||
/// Use this when you have a custom OpenAPI document for your API.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class ExcludeFromDefaultOpenApiDocumentAttribute : Attribute
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using System.IO.Pipelines;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.OpenApi;
|
||||
using Microsoft.OpenApi;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// Transformer to fix file return types in OpenAPI schema.
|
||||
/// </summary>
|
||||
/// <remarks>Can be removed once https://github.com/dotnet/aspnetcore/pull/63504 and
|
||||
/// https://github.com/dotnet/aspnetcore/pull/64562 are released.</remarks>
|
||||
internal class FixFileReturnTypesTransformer : IOpenApiSchemaTransformer
|
||||
{
|
||||
private static readonly Type[] _binaryStringTypes =
|
||||
[
|
||||
typeof(IFormFile),
|
||||
typeof(FileResult),
|
||||
typeof(Stream),
|
||||
typeof(PipeReader),
|
||||
];
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task TransformAsync(
|
||||
OpenApiSchema schema,
|
||||
OpenApiSchemaTransformerContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (_binaryStringTypes.Any(possibleBaseType => possibleBaseType.IsAssignableFrom(context.JsonTypeInfo.Type)) is false)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// Clear all properties
|
||||
schema.Properties?.Clear();
|
||||
schema.Required?.Clear();
|
||||
|
||||
// Make it an inline schema
|
||||
schema.Metadata?.Remove("x-schema-id");
|
||||
|
||||
// Set type to string with binary format
|
||||
schema.Type = JsonSchemaType.String;
|
||||
schema.Format = "binary";
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a method that determines whether a given API description should be included in a specific documentation
|
||||
/// document.
|
||||
/// </summary>
|
||||
public interface IDocumentInclusionSelector
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines whether the specified API description should be included in the generated documentation for the given
|
||||
/// document name.
|
||||
/// </summary>
|
||||
/// <param name="documentName">The name of the documentation document being generated.</param>
|
||||
/// <param name="apiDescription">The API description to evaluate for inclusion.</param>
|
||||
/// <returns>true if the API description should be included in the documentation; otherwise, false.</returns>
|
||||
bool Include(string documentName, ApiDescription apiDescription);
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a handler for generating OpenAPI operation IDs.
|
||||
/// </summary>
|
||||
public interface IOperationIdHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines whether this handler can generate an operation ID for the specified API description.
|
||||
/// </summary>
|
||||
/// <param name="apiDescription">The API description to check.</param>
|
||||
/// <returns><c>true</c> if this handler can handle the API description; otherwise, <c>false</c>.</returns>
|
||||
bool CanHandle(ApiDescription apiDescription);
|
||||
|
||||
/// <summary>
|
||||
/// Generates an operation ID for the specified API description.
|
||||
/// </summary>
|
||||
/// <param name="apiDescription">The API description to generate an operation ID for.</param>
|
||||
/// <returns>The generated operation ID.</returns>
|
||||
string Handle(ApiDescription apiDescription);
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a selector for choosing operation IDs from registered handlers.
|
||||
/// </summary>
|
||||
public interface IOperationIdSelector
|
||||
{
|
||||
/// <summary>
|
||||
/// Selects an operation ID for the specified API description.
|
||||
/// </summary>
|
||||
/// <param name="apiDescription">The API description to generate an operation ID for.</param>
|
||||
/// <returns>The operation ID, or <c>null</c> if none could be determined.</returns>
|
||||
string? OperationId(ApiDescription apiDescription);
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a handler for generating OpenAPI schema IDs.
|
||||
/// </summary>
|
||||
public interface ISchemaIdHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines whether this handler can generate a schema ID for the specified type.
|
||||
/// </summary>
|
||||
/// <param name="type">The type to check.</param>
|
||||
/// <returns><c>true</c> if this handler can handle the type; otherwise, <c>false</c>.</returns>
|
||||
bool CanHandle(Type type);
|
||||
|
||||
/// <summary>
|
||||
/// Generates a schema ID for the specified type.
|
||||
/// </summary>
|
||||
/// <param name="type">The type to generate a schema ID for.</param>
|
||||
/// <returns>The generated schema ID.</returns>
|
||||
string Handle(Type type);
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a selector for choosing schema IDs from registered handlers.
|
||||
/// </summary>
|
||||
public interface ISchemaIdSelector
|
||||
{
|
||||
/// <summary>
|
||||
/// Selects a schema ID for the specified type.
|
||||
/// </summary>
|
||||
/// <param name="type">The type to generate a schema ID for.</param>
|
||||
/// <returns>The schema ID.</returns>
|
||||
string SchemaId(Type type);
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a handler for discovering sub-types for polymorphic OpenAPI schemas.
|
||||
/// </summary>
|
||||
public interface ISubTypesHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines whether this handler can discover sub-types for the specified type and document.
|
||||
/// </summary>
|
||||
/// <param name="type">The type to check.</param>
|
||||
/// <param name="documentName">The OpenAPI document name.</param>
|
||||
/// <returns><c>true</c> if this handler can handle the type; otherwise, <c>false</c>.</returns>
|
||||
bool CanHandle(Type type, string documentName);
|
||||
|
||||
/// <summary>
|
||||
/// Discovers sub-types for the specified type.
|
||||
/// </summary>
|
||||
/// <param name="type">The type to discover sub-types for.</param>
|
||||
/// <returns>An enumerable of discovered sub-types.</returns>
|
||||
IEnumerable<Type> Handle(Type type);
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a selector for choosing sub-types from registered handlers.
|
||||
/// </summary>
|
||||
public interface ISubTypesSelector
|
||||
{
|
||||
/// <summary>
|
||||
/// Selects sub-types for the specified type for polymorphic OpenAPI schema generation.
|
||||
/// </summary>
|
||||
/// <param name="type">The type to find sub-types for.</param>
|
||||
/// <returns>An enumerable of sub-types.</returns>
|
||||
IEnumerable<Type> SubTypes(Type type);
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
using Microsoft.OpenApi;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// This filter explicitly removes all other mime types than application/json from a named OpenAPI document when application/json is accepted.
|
||||
/// </summary>
|
||||
public class MimeTypeDocumentFilter : IDocumentFilter
|
||||
{
|
||||
private readonly string _documentName;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MimeTypeDocumentFilter"/> class.
|
||||
/// </summary>
|
||||
/// <param name="documentName">The name of the OpenAPI document to filter.</param>
|
||||
public MimeTypeDocumentFilter(string documentName) => _documentName = documentName;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
|
||||
{
|
||||
if (context.DocumentName != _documentName)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
OpenApiOperation[] operations = swaggerDoc.Paths
|
||||
.SelectMany(path => path.Value.Operations?.Values ?? Enumerable.Empty<OpenApiOperation>())
|
||||
.ToArray();
|
||||
|
||||
static void RemoveUnwantedMimeTypes(IDictionary<string, OpenApiMediaType>? content)
|
||||
{
|
||||
if (content is null || content.ContainsKey("application/json") is false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
content.RemoveAll(r => r.Key != "application/json");
|
||||
}
|
||||
|
||||
OpenApiRequestBody[] requestBodies = operations
|
||||
.Select(operation => operation.RequestBody)
|
||||
.OfType<OpenApiRequestBody>()
|
||||
.ToArray();
|
||||
foreach (OpenApiRequestBody requestBody in requestBodies)
|
||||
{
|
||||
RemoveUnwantedMimeTypes(requestBody.Content);
|
||||
}
|
||||
|
||||
OpenApiResponse[] responses = operations
|
||||
.SelectMany(operation => operation.Responses?.Values ?? Enumerable.Empty<IOpenApiResponse>())
|
||||
.OfType<OpenApiResponse>()
|
||||
.ToArray();
|
||||
foreach (OpenApiResponse response in responses)
|
||||
{
|
||||
RemoveUnwantedMimeTypes(response.Content);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||
using Microsoft.AspNetCore.OpenApi;
|
||||
using Microsoft.OpenApi;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// Removes unwanted MIME types from OpenAPI operations, keeping only the content types
|
||||
/// declared by <c>[Consumes]</c> for request bodies or <c>application/json</c> as the default.
|
||||
/// </summary>
|
||||
internal class MimeTypesTransformer : IOpenApiOperationTransformer
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public Task TransformAsync(
|
||||
OpenApiOperation operation,
|
||||
OpenApiOperationTransformerContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// For request bodies, keep only the content types declared in [Consumes], or fall back to application/json.
|
||||
if (operation.RequestBody?.Content is { } requestContent)
|
||||
{
|
||||
var explicitContentTypes = context.Description.ActionDescriptor.EndpointMetadata
|
||||
.OfType<ConsumesAttribute>()
|
||||
.SelectMany(p => p.ContentTypes)
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
|
||||
if (explicitContentTypes.Length != 0)
|
||||
{
|
||||
// Replace content types entirely with what [Consumes] declares,
|
||||
// preserving the schema from the existing entry.
|
||||
OpenApiMediaType? existingMediaType = requestContent.Values.FirstOrDefault();
|
||||
requestContent.Clear();
|
||||
foreach (var contentType in explicitContentTypes)
|
||||
{
|
||||
requestContent[contentType] = existingMediaType ?? new OpenApiMediaType();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
RemoveNonJsonMimeTypes(requestContent);
|
||||
}
|
||||
}
|
||||
|
||||
// For responses, always keep only application/json.
|
||||
foreach (IOpenApiResponse response in (operation.Responses ?? []).Values)
|
||||
{
|
||||
if (response is OpenApiResponse openApiResponse)
|
||||
{
|
||||
RemoveNonJsonMimeTypes(openApiResponse.Content);
|
||||
}
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static void RemoveNonJsonMimeTypes(IDictionary<string, OpenApiMediaType>? content)
|
||||
{
|
||||
if (content?.ContainsKey("application/json") != true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
content.RemoveAll(r => r.Key != "application/json");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Swashbuckle.AspNetCore.SwaggerUI;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Web.Common.ApplicationBuilder;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
internal class OpenApiRouteTemplatePipelineFilter : UmbracoPipelineFilter
|
||||
{
|
||||
public OpenApiRouteTemplatePipelineFilter(string name)
|
||||
: base(name)
|
||||
{
|
||||
PostPipeline = PostPipelineAction;
|
||||
PreMapEndpoints = OnPreMapEndpointsAction;
|
||||
}
|
||||
|
||||
private static void PostPipelineAction(IApplicationBuilder applicationBuilder)
|
||||
{
|
||||
UmbracoOpenApiOptions options = applicationBuilder.ApplicationServices
|
||||
.GetRequiredService<IOptions<UmbracoOpenApiOptions>>().Value;
|
||||
|
||||
if (options.Enabled is false || options.DefaultUiEnabled is false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
applicationBuilder.UseSwaggerUI(swaggerUiOptions => ConfigureSwaggerUi(swaggerUiOptions, options));
|
||||
}
|
||||
|
||||
private static void OnPreMapEndpointsAction(IEndpointRouteBuilder endpoints)
|
||||
{
|
||||
UmbracoOpenApiOptions options = endpoints.ServiceProvider
|
||||
.GetRequiredService<IOptions<UmbracoOpenApiOptions>>().Value;
|
||||
|
||||
if (options.Enabled is false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
endpoints.MapOpenApi(options.RouteTemplate);
|
||||
}
|
||||
|
||||
private static void ConfigureSwaggerUi(SwaggerUIOptions swaggerUiOptions, UmbracoOpenApiOptions options)
|
||||
{
|
||||
swaggerUiOptions.RoutePrefix = options.UiRoutePrefix;
|
||||
|
||||
// Add custom configuration from https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/
|
||||
swaggerUiOptions.ConfigObject.PersistAuthorization = true; // persists authorization data so it would not be lost on browser close/refresh
|
||||
swaggerUiOptions.ConfigObject.Filter = string.Empty; // Enable the filter with an empty string as default filter.
|
||||
|
||||
swaggerUiOptions.OAuthClientId(Constants.OAuthClientIds.OpenApiUi);
|
||||
swaggerUiOptions.OAuthUsePkce();
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// Selects an operation ID for an API description using registered handlers.
|
||||
/// </summary>
|
||||
public class OperationIdSelector : IOperationIdSelector
|
||||
{
|
||||
private readonly IEnumerable<IOperationIdHandler> _operationIdHandlers;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OperationIdSelector"/> class.
|
||||
/// </summary>
|
||||
[Obsolete("Use non-obsolete constructor. Scheduled for removal in Umbraco 18.")]
|
||||
public OperationIdSelector()
|
||||
: this(Enumerable.Empty<IOperationIdHandler>())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OperationIdSelector"/> class.
|
||||
/// </summary>
|
||||
/// <param name="operationIdHandlers">The registered operation ID handlers.</param>
|
||||
public OperationIdSelector(IEnumerable<IOperationIdHandler> operationIdHandlers)
|
||||
=> _operationIdHandlers = operationIdHandlers;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual string? OperationId(ApiDescription apiDescription)
|
||||
{
|
||||
IOperationIdHandler? handler = _operationIdHandlers.FirstOrDefault(h => h.CanHandle(apiDescription));
|
||||
return handler?.Handle(apiDescription);
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
using Microsoft.OpenApi;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// This filter explicitly removes all security schemes from a named OpenAPI document.
|
||||
/// </summary>
|
||||
public class RemoveSecuritySchemesDocumentFilter : IDocumentFilter
|
||||
{
|
||||
private readonly string _documentName;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RemoveSecuritySchemesDocumentFilter"/> class.
|
||||
/// </summary>
|
||||
/// <param name="documentName">The name of the OpenAPI document to filter.</param>
|
||||
public RemoveSecuritySchemesDocumentFilter(string documentName)
|
||||
=> _documentName = documentName;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
|
||||
{
|
||||
if (context.DocumentName != _documentName)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
swaggerDoc.Components?.SecuritySchemes?.Clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using System.Reflection;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
using Microsoft.AspNetCore.OpenApi;
|
||||
using Microsoft.OpenApi;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// Ensures that all non-nullable properties are marked as required in the OpenAPI schema.
|
||||
/// </summary>
|
||||
/// <remarks>By default, only properties marked with the required keyword will actually show as required.
|
||||
/// Non-nullable reference types were not taken into account.</remarks>
|
||||
internal class RequireNonNullablePropertiesSchemaTransformer : IOpenApiSchemaTransformer
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
IEnumerable<string> additionalRequiredProps = schema.Properties?
|
||||
.Where(p => schema.Required?.Contains(p.Key) != true) // If it's already required, skip
|
||||
.Where(x => IsRequiredProperty(schema, context.JsonTypeInfo, x.Key))
|
||||
.Select(x => x.Key)
|
||||
?? [];
|
||||
schema.Required ??= new HashSet<string>();
|
||||
foreach (var propKey in additionalRequiredProps)
|
||||
{
|
||||
schema.Required.Add(propKey);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static bool IsRequiredProperty(OpenApiSchema schema, JsonTypeInfo jsonTypeInfo, string propertyName)
|
||||
{
|
||||
if (jsonTypeInfo.Properties.FirstOrDefault(p => p.Name == propertyName) is { } property)
|
||||
{
|
||||
return property.IsGetNullable is false;
|
||||
}
|
||||
|
||||
// If we can't find the property in the type (e.g. discriminator '$type'), use the schema type information.
|
||||
if (schema.Properties?.TryGetValue(propertyName, out IOpenApiSchema? schemaProperty) is true
|
||||
&& schemaProperty?.Type is { } propertyType)
|
||||
{
|
||||
return propertyType.HasFlag(JsonSchemaType.Null) is false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// Selects a schema ID for a type using registered handlers.
|
||||
/// </summary>
|
||||
public class SchemaIdSelector : ISchemaIdSelector
|
||||
{
|
||||
private readonly IEnumerable<ISchemaIdHandler> _schemaIdHandlers;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SchemaIdSelector"/> class.
|
||||
/// </summary>
|
||||
/// <param name="schemaIdHandlers">The registered schema ID handlers.</param>
|
||||
public SchemaIdSelector(IEnumerable<ISchemaIdHandler> schemaIdHandlers)
|
||||
=> _schemaIdHandlers = schemaIdHandlers;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual string SchemaId(Type type)
|
||||
{
|
||||
ISchemaIdHandler? handler = _schemaIdHandlers.FirstOrDefault(h => h.CanHandle(type));
|
||||
return handler?.Handle(type) ?? type.Name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using Microsoft.AspNetCore.OpenApi;
|
||||
using Microsoft.OpenApi;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// Transforms the OpenAPI document to sort tags and paths alphabetically.
|
||||
/// </summary>
|
||||
internal class SortTagsAndPathsTransformer : IOpenApiDocumentTransformer
|
||||
{
|
||||
/// <summary>
|
||||
/// Transforms the specified OpenAPI document to sort its tags and paths alphabetically.
|
||||
/// </summary>
|
||||
/// <param name="document">The <see cref="OpenApiDocument"/> to modify.</param>
|
||||
/// <param name="context">The <see cref="OpenApiDocumentTransformerContext"/> associated with the <paramref name="document"/>.</param>
|
||||
/// <param name="cancellationToken">The cancellation token to use.</param>
|
||||
/// <returns>The task object representing the asynchronous operation.</returns>
|
||||
public Task TransformAsync(
|
||||
OpenApiDocument document,
|
||||
OpenApiDocumentTransformerContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
document.Tags = new SortedSet<OpenApiTag>(
|
||||
document.Tags ?? Enumerable.Empty<OpenApiTag>(),
|
||||
Comparer<OpenApiTag>.Create((a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal)));
|
||||
|
||||
var sortedPaths = new OpenApiPaths();
|
||||
foreach (KeyValuePair<string, IOpenApiPathItem> keyValuePair in document.Paths
|
||||
.OrderBy(x => x.Value.Operations?.Values
|
||||
.SelectMany(op => op.Tags ?? Enumerable.Empty<OpenApiTagReference>())
|
||||
.OrderBy(t => t.Name)
|
||||
.FirstOrDefault()?
|
||||
.Name)
|
||||
.ThenBy(x => x.Key))
|
||||
{
|
||||
sortedPaths.Add(keyValuePair.Key, keyValuePair.Value);
|
||||
}
|
||||
|
||||
document.Paths = sortedPaths;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
using Umbraco.Cms.Api.Common.Serialization;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// Default handler for discovering sub-types for polymorphic OpenAPI schemas.
|
||||
/// </summary>
|
||||
public class SubTypesHandler : ISubTypesHandler
|
||||
{
|
||||
private readonly IUmbracoJsonTypeInfoResolver _umbracoJsonTypeInfoResolver;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SubTypesHandler"/> class.
|
||||
/// </summary>
|
||||
/// <param name="umbracoJsonTypeInfoResolver">The JSON type info resolver for finding sub-types.</param>
|
||||
public SubTypesHandler(IUmbracoJsonTypeInfoResolver umbracoJsonTypeInfoResolver)
|
||||
=> _umbracoJsonTypeInfoResolver = umbracoJsonTypeInfoResolver;
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether this handler can process the specified type based on namespace.
|
||||
/// </summary>
|
||||
/// <param name="type">The type to check.</param>
|
||||
/// <returns><c>true</c> if the type is in an Umbraco.Cms namespace; otherwise, <c>false</c>.</returns>
|
||||
protected virtual bool CanHandle(Type type)
|
||||
=> type.Namespace?.StartsWith("Umbraco.Cms") is true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual bool CanHandle(Type type, string documentName)
|
||||
=> CanHandle(type);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual IEnumerable<Type> Handle(Type type)
|
||||
=> _umbracoJsonTypeInfoResolver.FindSubTypes(type);
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Api.Common.Serialization;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Hosting;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// Selects sub-types for polymorphic OpenAPI schemas using registered handlers.
|
||||
/// </summary>
|
||||
public class SubTypesSelector : ISubTypesSelector
|
||||
{
|
||||
private readonly IHostingEnvironment _hostingEnvironment;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly IEnumerable<ISubTypesHandler> _subTypeHandlers;
|
||||
private readonly IUmbracoJsonTypeInfoResolver _umbracoJsonTypeInfoResolver;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SubTypesSelector"/> class.
|
||||
/// </summary>
|
||||
/// <param name="hostingEnvironment">The hosting environment.</param>
|
||||
/// <param name="httpContextAccessor">The HTTP context accessor.</param>
|
||||
/// <param name="subTypeHandlers">The registered sub-type handlers.</param>
|
||||
/// <param name="umbracoJsonTypeInfoResolver">The JSON type info resolver for finding sub-types.</param>
|
||||
public SubTypesSelector(
|
||||
IHostingEnvironment hostingEnvironment,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IEnumerable<ISubTypesHandler> subTypeHandlers,
|
||||
IUmbracoJsonTypeInfoResolver umbracoJsonTypeInfoResolver)
|
||||
{
|
||||
_hostingEnvironment = hostingEnvironment;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_subTypeHandlers = subTypeHandlers;
|
||||
_umbracoJsonTypeInfoResolver = umbracoJsonTypeInfoResolver;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<Type> SubTypes(Type type)
|
||||
{
|
||||
var backOfficePath = _hostingEnvironment.GetBackOfficePath();
|
||||
var swaggerPath = $"{backOfficePath}/swagger";
|
||||
|
||||
if (_httpContextAccessor.HttpContext?.Request.Path.StartsWithSegments(swaggerPath) ?? false)
|
||||
{
|
||||
// Split the path into segments
|
||||
var segments = _httpContextAccessor.HttpContext.Request.Path.Value![swaggerPath.Length..]
|
||||
.TrimStart(Constants.CharArrays.ForwardSlash)
|
||||
.Split(Constants.CharArrays.ForwardSlash);
|
||||
|
||||
// Extract the document name from the path
|
||||
var documentName = segments[0];
|
||||
|
||||
// Find the first handler that can handle the type / document name combination
|
||||
ISubTypesHandler? handler = _subTypeHandlers.FirstOrDefault(h => h.CanHandle(type, documentName));
|
||||
if (handler != null)
|
||||
{
|
||||
return handler.Handle(type);
|
||||
}
|
||||
}
|
||||
|
||||
// Default implementation to maintain backwards compatibility
|
||||
return _umbracoJsonTypeInfoResolver.FindSubTypes(type);
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.OpenApi;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using Swashbuckle.AspNetCore.SwaggerUI;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Hosting;
|
||||
using Umbraco.Cms.Web.Common.ApplicationBuilder;
|
||||
using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// Pipeline filter that configures Swagger/OpenAPI endpoints for Umbraco APIs.
|
||||
/// </summary>
|
||||
public class SwaggerRouteTemplatePipelineFilter : UmbracoPipelineFilter
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SwaggerRouteTemplatePipelineFilter"/> class.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the pipeline filter.</param>
|
||||
public SwaggerRouteTemplatePipelineFilter(string name)
|
||||
: base(name)
|
||||
=> PostPipeline = PostPipelineAction;
|
||||
|
||||
private void PostPipelineAction(IApplicationBuilder applicationBuilder)
|
||||
{
|
||||
if (SwaggerIsEnabled(applicationBuilder) is false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IOptions<SwaggerGenOptions> swaggerGenOptions = applicationBuilder.ApplicationServices.GetRequiredService<IOptions<SwaggerGenOptions>>();
|
||||
|
||||
applicationBuilder.UseSwagger(swaggerOptions =>
|
||||
{
|
||||
swaggerOptions.RouteTemplate = SwaggerRouteTemplate(applicationBuilder);
|
||||
});
|
||||
|
||||
applicationBuilder.UseSwaggerUI(swaggerUiOptions => SwaggerUiConfiguration(swaggerUiOptions, swaggerGenOptions.Value, applicationBuilder));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether Swagger is enabled for the application.
|
||||
/// </summary>
|
||||
/// <param name="applicationBuilder">The application builder.</param>
|
||||
/// <returns><c>true</c> if Swagger is enabled; otherwise, <c>false</c>.</returns>
|
||||
protected virtual bool SwaggerIsEnabled(IApplicationBuilder applicationBuilder)
|
||||
=> applicationBuilder.ApplicationServices.GetRequiredService<IWebHostEnvironment>().IsProduction() is false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the route template for Swagger JSON endpoints.
|
||||
/// </summary>
|
||||
/// <param name="applicationBuilder">The application builder.</param>
|
||||
/// <returns>The Swagger route template.</returns>
|
||||
protected virtual string SwaggerRouteTemplate(IApplicationBuilder applicationBuilder)
|
||||
=> $"{GetBackOfficePath(applicationBuilder).TrimStart(Constants.CharArrays.ForwardSlash)}/swagger/{{documentName}}/swagger.json";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the route prefix for the Swagger UI.
|
||||
/// </summary>
|
||||
/// <param name="applicationBuilder">The application builder.</param>
|
||||
/// <returns>The Swagger UI route prefix.</returns>
|
||||
protected virtual string SwaggerUiRoutePrefix(IApplicationBuilder applicationBuilder)
|
||||
=> $"{GetBackOfficePath(applicationBuilder).TrimStart(Constants.CharArrays.ForwardSlash)}/swagger";
|
||||
|
||||
/// <summary>
|
||||
/// Configures the Swagger UI options.
|
||||
/// </summary>
|
||||
/// <param name="swaggerUiOptions">The Swagger UI options to configure.</param>
|
||||
/// <param name="swaggerGenOptions">The Swagger generation options.</param>
|
||||
/// <param name="applicationBuilder">The application builder.</param>
|
||||
protected virtual void SwaggerUiConfiguration(
|
||||
SwaggerUIOptions swaggerUiOptions,
|
||||
SwaggerGenOptions swaggerGenOptions,
|
||||
IApplicationBuilder applicationBuilder)
|
||||
{
|
||||
swaggerUiOptions.RoutePrefix = SwaggerUiRoutePrefix(applicationBuilder);
|
||||
|
||||
foreach ((var name, OpenApiInfo? apiInfo) in swaggerGenOptions.SwaggerGeneratorOptions.SwaggerDocs.OrderBy(x => x.Value.Title))
|
||||
{
|
||||
swaggerUiOptions.SwaggerEndpoint($"{name}/swagger.json", $"{apiInfo.Title}");
|
||||
}
|
||||
|
||||
// Add custom configuration from https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/
|
||||
swaggerUiOptions.ConfigObject.PersistAuthorization = true; // persists authorization data so it would not be lost on browser close/refresh
|
||||
swaggerUiOptions.ConfigObject.Filter = string.Empty; // Enable the filter with an empty string as default filter.
|
||||
|
||||
swaggerUiOptions.OAuthClientId(Constants.OAuthClientIds.Swagger);
|
||||
swaggerUiOptions.OAuthUsePkce();
|
||||
}
|
||||
|
||||
private string GetBackOfficePath(IApplicationBuilder applicationBuilder)
|
||||
=> applicationBuilder.ApplicationServices.GetRequiredService<IHostingEnvironment>().GetBackOfficePath();
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using Microsoft.AspNetCore.OpenApi;
|
||||
using Microsoft.OpenApi;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// Transformer that tags operations based on their group name.
|
||||
/// </summary>
|
||||
internal class TagActionsByGroupNameTransformer : IOpenApiOperationTransformer, IOpenApiDocumentTransformer
|
||||
{
|
||||
/// <summary>
|
||||
/// Transforms the specified OpenAPI operation in order to tag it by its group name.
|
||||
/// </summary>
|
||||
/// <param name="operation">The <see cref="OpenApiOperation"/> to modify.</param>
|
||||
/// <param name="context">The <see cref="OpenApiOperationTransformerContext"/> associated with the <paramref name="operation"/>.</param>
|
||||
/// <param name="cancellationToken">The cancellation token to use.</param>
|
||||
/// <returns>The task object representing the asynchronous operation.</returns>
|
||||
public Task TransformAsync(
|
||||
OpenApiOperation operation,
|
||||
OpenApiOperationTransformerContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (context.Document is null || context.Description.GroupName is not { } groupName)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
operation.Tags = new HashSet<OpenApiTagReference> { new(groupName) };
|
||||
if (context.Document.Tags?.Any(t => t.Name == groupName) == true)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
context.Document.Tags ??= new HashSet<OpenApiTag>();
|
||||
context.Document.Tags.Add(new OpenApiTag { Name = groupName });
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transforms the specified OpenAPI document in order to clean up unused tags.
|
||||
/// </summary>
|
||||
/// <param name="document">The <see cref="OpenApiDocument"/> to modify.</param>
|
||||
/// <param name="context">The <see cref="OpenApiDocumentTransformerContext"/> associated with the <paramref name="document"/>.</param>
|
||||
/// <param name="cancellationToken">The cancellation token to use.</param>
|
||||
/// <returns>The task object representing the asynchronous operation.</returns>
|
||||
public Task TransformAsync(
|
||||
OpenApiDocument document,
|
||||
OpenApiDocumentTransformerContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var usedTags = new HashSet<string?>(document.Paths
|
||||
.SelectMany(p => (p.Value.Operations ?? []).Values)
|
||||
.SelectMany(o => o.Tags ?? new HashSet<OpenApiTagReference>())
|
||||
.Select(t => t.Name));
|
||||
|
||||
var tagsToRemove = (document.Tags ?? Enumerable.Empty<OpenApiTag>())
|
||||
.Where(tag => usedTags.Contains(tag.Name) is false)
|
||||
.ToList();
|
||||
|
||||
foreach (OpenApiTag tag in tagsToRemove)
|
||||
{
|
||||
document.Tags?.Remove(tag);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// Options for configuring OpenAPI documents and UI.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// These options are populated by <c>AddUmbracoOpenApi</c> during DI configuration, which resolves the back-office path
|
||||
/// from <see cref="Core.Hosting.IHostingEnvironment"/> and sets the default values for
|
||||
/// <see cref="RouteTemplate"/> and <see cref="UiRoutePrefix"/>. Consumers that read this options type before
|
||||
/// <c>AddUmbracoOpenApi</c> has run will observe the uninitialised defaults (empty strings for the route properties).
|
||||
/// </remarks>
|
||||
public class UmbracoOpenApiOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets whether OpenAPI documents are enabled.
|
||||
/// Configured to <c>true</c> in non-production environments by default; <c>false</c> until configured.
|
||||
/// This avoids exposing API structure on public-facing websites.
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the default OpenAPI UI is enabled.
|
||||
/// Only applies when <see cref="Enabled"/> is true.
|
||||
/// Set to false to disable the default UI while keeping OpenAPI documents available,
|
||||
/// allowing you to use an alternative UI.
|
||||
/// Default: true.
|
||||
/// </summary>
|
||||
public bool DefaultUiEnabled { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the route template for OpenAPI JSON documents.
|
||||
/// Use <c>{documentName}</c> as a placeholder for the document name.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Populated by <c>AddUmbracoOpenApi</c> to <c>"{backOfficePath}/openapi/{documentName}.json"</c>. The initial
|
||||
/// <see cref="string.Empty"/> default is a sentinel for "not yet configured" — it is not a usable route template.
|
||||
/// </remarks>
|
||||
public string RouteTemplate { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the route prefix for OpenAPI UI.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Populated by <c>AddUmbracoOpenApi</c> to <c>"{backOfficePath}/openapi"</c>. The initial <see cref="string.Empty"/>
|
||||
/// default is a sentinel for "not yet configured" — it is not a usable route prefix.
|
||||
/// </remarks>
|
||||
public string UiRoutePrefix { get; set; } = string.Empty;
|
||||
}
|
||||
+20
-36
@@ -1,63 +1,47 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.AspNetCore.OpenApi;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.OpenApi;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// Default handler for generating OpenAPI operation IDs for Umbraco API controllers.
|
||||
/// Transforms OpenAPI operation IDs using Umbraco's naming conventions.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Left unsealed on purpose, so it is extendable by consuming APIs.
|
||||
/// This transformer can be registered manually for custom OpenAPI configurations.
|
||||
/// </remarks>
|
||||
public class OperationIdHandler : IOperationIdHandler
|
||||
public class UmbracoOperationIdTransformer : IOpenApiOperationTransformer
|
||||
{
|
||||
private readonly ApiVersioningOptions _apiVersioningOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OperationIdHandler"/> class.
|
||||
/// Transforms the specified OpenAPI operation, setting its operation ID using a custom selector.
|
||||
/// </summary>
|
||||
/// <param name="apiVersioningOptions">The API versioning options.</param>
|
||||
public OperationIdHandler(IOptions<ApiVersioningOptions> apiVersioningOptions)
|
||||
=> _apiVersioningOptions = apiVersioningOptions.Value;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool CanHandle(ApiDescription apiDescription)
|
||||
/// <param name="operation">The <see cref="OpenApiOperation"/> to modify.</param>
|
||||
/// <param name="context">The <see cref="OpenApiOperationTransformerContext"/> associated with the <paramref name="operation"/>.</param>
|
||||
/// <param name="cancellationToken">The cancellation token to use.</param>
|
||||
/// <returns>The task object representing the asynchronous operation.</returns>
|
||||
public Task TransformAsync(
|
||||
OpenApiOperation operation,
|
||||
OpenApiOperationTransformerContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (apiDescription.ActionDescriptor is not ControllerActionDescriptor controllerActionDescriptor)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return CanHandle(apiDescription, controllerActionDescriptor);
|
||||
operation.OperationId = GenerateOperationId(context);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether this handler can process the API description based on the controller namespace.
|
||||
/// </summary>
|
||||
/// <param name="apiDescription">The API description.</param>
|
||||
/// <param name="controllerActionDescriptor">The controller action descriptor.</param>
|
||||
/// <returns><c>true</c> if the controller is in an Umbraco.Cms.Api namespace; otherwise, <c>false</c>.</returns>
|
||||
protected virtual bool CanHandle(ApiDescription apiDescription, ControllerActionDescriptor controllerActionDescriptor)
|
||||
=> controllerActionDescriptor.ControllerTypeInfo.Namespace?.StartsWith("Umbraco.Cms.Api") is true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual string Handle(ApiDescription apiDescription)
|
||||
=> UmbracoOperationId(apiDescription);
|
||||
|
||||
/// <summary>
|
||||
/// Generates a unique operation identifier for a given API following Umbraco's operation id naming conventions.
|
||||
/// </summary>
|
||||
protected string UmbracoOperationId(ApiDescription apiDescription)
|
||||
private static string GenerateOperationId(OpenApiOperationTransformerContext context)
|
||||
{
|
||||
ApiDescription apiDescription = context.Description;
|
||||
if (apiDescription.ActionDescriptor is not ControllerActionDescriptor controllerActionDescriptor)
|
||||
{
|
||||
throw new ArgumentException($"This handler operates only on {nameof(ControllerActionDescriptor)}.");
|
||||
}
|
||||
|
||||
ApiVersion defaultVersion = _apiVersioningOptions.DefaultApiVersion;
|
||||
ApiVersion defaultVersion = context.ApplicationServices.GetRequiredService<IOptions<ApiVersioningOptions>>().Value.DefaultApiVersion;
|
||||
var httpMethod = apiDescription.HttpMethod?.ToLower().ToFirstUpper() ?? "Get";
|
||||
|
||||
// if the route info "Name" is supplied we'll use this explicitly as the operation ID
|
||||
+8
-19
@@ -4,29 +4,18 @@ using Umbraco.Extensions;
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// Default handler for generating OpenAPI schema IDs for Umbraco types.
|
||||
/// Static utility for generating OpenAPI schema IDs following Umbraco's naming conventions.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Left unsealed on purpose, so it is extendable by consuming APIs.
|
||||
/// Adds "Model" suffix to avoid TypeScript name clashes and removes invalid characters.
|
||||
/// </remarks>
|
||||
public class SchemaIdHandler : ISchemaIdHandler
|
||||
public static class UmbracoSchemaIdGenerator
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public virtual bool CanHandle(Type type)
|
||||
=> type.Namespace?.StartsWith("Umbraco.Cms") is true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual string Handle(Type type)
|
||||
=> UmbracoSchemaId(type);
|
||||
|
||||
/// <summary>
|
||||
/// Generates a sanitized and consistent schema identifier for a given type following Umbraco's schema id naming conventions.
|
||||
/// Generates a schema ID for the specified type following Umbraco's naming conventions.
|
||||
/// </summary>
|
||||
protected string UmbracoSchemaId(Type type)
|
||||
/// <param name="type">The type to generate a schema ID for.</param>
|
||||
/// <returns>The generated schema ID.</returns>
|
||||
public static string Generate(Type type)
|
||||
{
|
||||
var name = SanitizedTypeName(type);
|
||||
|
||||
name = HandleGenerics(name, type);
|
||||
|
||||
if (name.EndsWith("Model") == false)
|
||||
@@ -40,13 +29,13 @@ public class SchemaIdHandler : ISchemaIdHandler
|
||||
return Regex.Replace(name, @"[^\w]", string.Empty);
|
||||
}
|
||||
|
||||
private string SanitizedTypeName(Type t) => t.Name
|
||||
private static string SanitizedTypeName(Type t) => t.Name
|
||||
// first grab the "non-generic" part of any generic type name (i.e. "PagedViewModel`1" becomes "PagedViewModel")
|
||||
.Split('`').First()
|
||||
// then remove the "ViewModel" postfix from type names
|
||||
.TrimEnd("ViewModel");
|
||||
|
||||
private string HandleGenerics(string name, Type type)
|
||||
private static string HandleGenerics(string name, Type type)
|
||||
{
|
||||
if (!type.IsGenericType)
|
||||
{
|
||||
+3
-2
@@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Options;
|
||||
using OpenIddict.Server;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Extensions;
|
||||
@@ -32,12 +33,12 @@ public class ExposeBackOfficeAuthenticationOpenIddictServerEventsHandler : IOpen
|
||||
|
||||
// These are the type identifiers for the claims required by the principal
|
||||
// for the custom authentication scheme.
|
||||
// We make available the ID, user name and allowed applications (sections) claims.
|
||||
// We make available the ID and user name claims, plus the claim necessary for parsing the user key.
|
||||
_claimTypes =
|
||||
[
|
||||
backOfficeIdentityOptions.Value.ClaimsIdentity.UserIdClaimType,
|
||||
backOfficeIdentityOptions.Value.ClaimsIdentity.UserNameClaimType,
|
||||
Core.Constants.Security.AllowedApplicationsClaimType,
|
||||
Constants.Security.OpenIdDictSubClaimType
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,12 @@
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||
<_Parameter1>Umbraco.Tests.UnitTests</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||
<_Parameter1>Umbraco.Cms.Api.Management</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||
<_Parameter1>Umbraco.Cms.Api.Delivery</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -15,11 +21,12 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Asp.Versioning.Mvc "/>
|
||||
<PackageReference Include="Asp.Versioning.Mvc" />
|
||||
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
|
||||
<PackageReference Include="OpenIddict.Abstractions" />
|
||||
<PackageReference Include="OpenIddict.AspNetCore" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -39,7 +39,7 @@ Umbraco.Cms.Api.Delivery/
|
||||
├── Services/ # Business logic and query building
|
||||
├── Caching/ # Output cache policies
|
||||
├── Rendering/ # Output expansion strategies
|
||||
├── Configuration/ # Swagger configuration
|
||||
├── Configuration/ # OpenAPI configuration
|
||||
└── Filters/ # Action filters (access, validation)
|
||||
```
|
||||
|
||||
@@ -200,10 +200,9 @@ context.EnableOutputCaching = requestPreviewService.IsPreview() is false
|
||||
|
||||
### Technical Debt (TODOs in codebase)
|
||||
|
||||
1. **V1 Removal Pending** (4 locations):
|
||||
1. **V1 Removal Pending** (2 locations):
|
||||
- `DependencyInjection/UmbracoBuilderExtensions.cs:98` - FIXME: remove matcher policy
|
||||
- `Routing/DeliveryApiItemsEndpointsMatcherPolicy.cs:11` - FIXME: remove class
|
||||
- `Filters/SwaggerDocumentationFilterBase.cs:79,83` - FIXME: remove V1 swagger docs
|
||||
|
||||
2. **Obsolete Reference Warnings** (csproj:9-13):
|
||||
- `ASP0019` - IHeaderDictionary.Append usage
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
using Microsoft.AspNetCore.OutputCaching;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.Changes;
|
||||
using Umbraco.Cms.Core.Sync;
|
||||
using Umbraco.Cms.Web.Common.Caching;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Caching;
|
||||
|
||||
/// <summary>
|
||||
/// Handles <see cref="ElementCacheRefresherNotification"/> to evict Delivery API output cache entries
|
||||
/// for content that references the changed element via picker properties (umbElement relations).
|
||||
/// </summary>
|
||||
internal sealed class DeliveryApiElementOutputCacheEvictionHandler
|
||||
: RelationOutputCacheEvictionHandlerBase, INotificationAsyncHandler<ElementCacheRefresherNotification>
|
||||
{
|
||||
private readonly ILogger<DeliveryApiElementOutputCacheEvictionHandler> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DeliveryApiElementOutputCacheEvictionHandler"/> class.
|
||||
/// </summary>
|
||||
/// <param name="outputCacheStore">The output cache store for evicting cached responses.</param>
|
||||
/// <param name="relationService">The relation service for querying entity references.</param>
|
||||
/// <param name="idKeyMap">The ID/key mapping service for converting between integer IDs and GUIDs.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public DeliveryApiElementOutputCacheEvictionHandler(
|
||||
IOutputCacheStore outputCacheStore,
|
||||
IRelationService relationService,
|
||||
IIdKeyMap idKeyMap,
|
||||
ILogger<DeliveryApiElementOutputCacheEvictionHandler> logger)
|
||||
: base(outputCacheStore, relationService, idKeyMap)
|
||||
=> _logger = logger;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task HandleAsync(ElementCacheRefresherNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
if (notification.MessageType != MessageType.RefreshByPayload
|
||||
|| notification.MessageObject is not ElementCacheRefresher.JsonPayload[] payloads)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (ElementCacheRefresher.JsonPayload payload in payloads)
|
||||
{
|
||||
if (payload.ChangeTypes.HasFlag(TreeChangeTypes.RefreshAll))
|
||||
{
|
||||
// Evict all Delivery API responses — content responses may include referenced elements,
|
||||
// so evicting only element-related entries would leave stale element references in content responses.
|
||||
_logger.LogDebug("Element refresh all — evicting all Delivery API output cache entries.");
|
||||
await OutputCacheStore.EvictByTagAsync(Constants.DeliveryApi.OutputCache.AllTag, cancellationToken);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Evict content that references the changed elements via picker properties.
|
||||
await EvictRelatedContentAsync(
|
||||
payloads.Select(p => p.Id),
|
||||
Constants.Conventions.RelationTypes.RelatedElementAlias,
|
||||
Constants.DeliveryApi.OutputCache.ContentTagPrefix,
|
||||
_logger,
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
using Microsoft.AspNetCore.OpenApi;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Api.Common.Configuration;
|
||||
using Umbraco.Cms.Api.Common.OpenApi;
|
||||
using Umbraco.Cms.Api.Delivery.OpenApi.Transformers;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Configures the OpenAPI options for the Umbraco Delivery API.
|
||||
/// </summary>
|
||||
internal class ConfigureUmbracoDeliveryApiOpenApiOptions : ConfigureUmbracoOpenApiOptionsBase
|
||||
{
|
||||
private readonly DeliveryApiSettings _deliveryApiSettings;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConfigureUmbracoDeliveryApiOpenApiOptions"/> class.
|
||||
/// </summary>
|
||||
/// <param name="deliveryApiSettings">The Delivery API settings.</param>
|
||||
public ConfigureUmbracoDeliveryApiOpenApiOptions(IOptions<DeliveryApiSettings> deliveryApiSettings)
|
||||
{
|
||||
_deliveryApiSettings = deliveryApiSettings.Value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string ApiName => DeliveryApiConfiguration.ApiName;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string ApiTitle => DeliveryApiConfiguration.ApiTitle;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string ApiVersion => "Latest";
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string ApiDescription =>
|
||||
$"You can find out more about the {DeliveryApiConfiguration.ApiTitle} in [the documentation]({DeliveryApiConfiguration.ApiDocumentationContentArticleLink}).";
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void ConfigureOpenApi(OpenApiOptions options)
|
||||
{
|
||||
base.ConfigureOpenApi(options);
|
||||
|
||||
// Add API key security scheme and configure it for all operations
|
||||
options
|
||||
.AddDocumentTransformer<ApiKeyTransformer>()
|
||||
.AddOperationTransformer<ApiKeyTransformer>();
|
||||
|
||||
options.AddSchemaTransformer<RequireNonNullablePropertiesSchemaTransformer>();
|
||||
options.AddSchemaTransformer<FixFileReturnTypesTransformer>();
|
||||
options.AddOperationTransformer<MimeTypesTransformer>();
|
||||
options.AddOperationTransformer<ContentApiTransformer>();
|
||||
options.AddOperationTransformer<MediaApiTransformer>();
|
||||
|
||||
if (_deliveryApiSettings.OpenApi.GenerateContentTypeSchemas)
|
||||
{
|
||||
options
|
||||
.AddSchemaTransformer<ContentTypeSchemaTransformer>()
|
||||
.AddDocumentTransformer<ContentTypeSchemaTransformer>();
|
||||
}
|
||||
}
|
||||
}
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.OpenApi;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using Umbraco.Cms.Api.Common.OpenApi;
|
||||
using Umbraco.Cms.Api.Delivery.Filters;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Configuration;
|
||||
|
||||
public class ConfigureUmbracoDeliveryApiSwaggerGenOptions: IConfigureOptions<SwaggerGenOptions>
|
||||
{
|
||||
public void Configure(SwaggerGenOptions swaggerGenOptions)
|
||||
{
|
||||
swaggerGenOptions.SwaggerDoc(
|
||||
DeliveryApiConfiguration.ApiName,
|
||||
new OpenApiInfo
|
||||
{
|
||||
Title = DeliveryApiConfiguration.ApiTitle,
|
||||
Version = "Latest",
|
||||
Description = $"You can find out more about the {DeliveryApiConfiguration.ApiTitle} in [the documentation]({DeliveryApiConfiguration.ApiDocumentationContentArticleLink})."
|
||||
});
|
||||
|
||||
swaggerGenOptions.DocumentFilter<MimeTypeDocumentFilter>(DeliveryApiConfiguration.ApiName);
|
||||
swaggerGenOptions.DocumentFilter<RemoveSecuritySchemesDocumentFilter>(DeliveryApiConfiguration.ApiName);
|
||||
|
||||
swaggerGenOptions.OperationFilter<SwaggerContentDocumentationFilter>();
|
||||
swaggerGenOptions.OperationFilter<SwaggerMediaDocumentationFilter>();
|
||||
swaggerGenOptions.ParameterFilter<SwaggerContentDocumentationFilter>();
|
||||
swaggerGenOptions.ParameterFilter<SwaggerMediaDocumentationFilter>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.AspNetCore.Http.Json;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Configures the Http JSON options for the Umbraco Delivery API.
|
||||
/// </summary>
|
||||
internal class ConfigureUmbracoDeliveryHttpJsonOptions : IConfigureNamedOptions<JsonOptions>
|
||||
{
|
||||
private readonly IOptionsMonitor<Microsoft.AspNetCore.Mvc.JsonOptions> _mvcJsonOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConfigureUmbracoDeliveryHttpJsonOptions"/> class.
|
||||
/// </summary>
|
||||
/// <param name="mvcJsonOptions">The configured MVC json options.</param>
|
||||
public ConfigureUmbracoDeliveryHttpJsonOptions(IOptionsMonitor<Microsoft.AspNetCore.Mvc.JsonOptions> mvcJsonOptions)
|
||||
=> _mvcJsonOptions = mvcJsonOptions;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Configure(JsonOptions options) => Configure(Options.DefaultName, options);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Configure(string? name, JsonOptions options)
|
||||
{
|
||||
if (name != Constants.JsonOptionsNames.DeliveryApi)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Copy all converters from the Delivery API MVC JSON options
|
||||
Microsoft.AspNetCore.Mvc.JsonOptions backofficeMvcJsonOptions = _mvcJsonOptions.Get(Constants.JsonOptionsNames.DeliveryApi);
|
||||
foreach (JsonConverter jsonConverter in backofficeMvcJsonOptions.JsonSerializerOptions.Converters)
|
||||
{
|
||||
options.SerializerOptions.Converters.Add(jsonConverter);
|
||||
}
|
||||
|
||||
options.SerializerOptions.PropertyNamingPolicy = backofficeMvcJsonOptions.JsonSerializerOptions.PropertyNamingPolicy;
|
||||
options.SerializerOptions.TypeInfoResolver = backofficeMvcJsonOptions.JsonSerializerOptions.TypeInfoResolver;
|
||||
options.SerializerOptions.MaxDepth = backofficeMvcJsonOptions.JsonSerializerOptions.MaxDepth;
|
||||
|
||||
// Open API specific settings
|
||||
options.SerializerOptions.NumberHandling = JsonNumberHandling.Strict;
|
||||
}
|
||||
}
|
||||
-69
@@ -1,69 +0,0 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.OpenApi;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using Umbraco.Cms.Api.Common.Security;
|
||||
using Umbraco.Cms.Api.Delivery.Controllers.Content;
|
||||
using Umbraco.Cms.Api.Delivery.Filters;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// This configures member authentication for the Delivery API in Swagger. Consult the docs for
|
||||
/// member authentication within the Delivery API for instructions on how to use this.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This class is not used by the core CMS due to the required installation dependencies (local login page among other things).
|
||||
/// </remarks>
|
||||
public class ConfigureUmbracoMemberAuthenticationDeliveryApiSwaggerGenOptions : IConfigureOptions<SwaggerGenOptions>
|
||||
{
|
||||
private const string AuthSchemeName = "UmbracoMember";
|
||||
|
||||
public void Configure(SwaggerGenOptions options)
|
||||
{
|
||||
// add security requirements for content API operations
|
||||
options.DocumentFilter<DeliveryApiSecurityFilter>();
|
||||
options.OperationFilter<DeliveryApiSecurityFilter>();
|
||||
}
|
||||
|
||||
private sealed class DeliveryApiSecurityFilter : SwaggerFilterBase<ContentApiControllerBase>, IOperationFilter, IDocumentFilter
|
||||
{
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
if (CanApply(context) is false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var schemaRef = new OpenApiSecuritySchemeReference(AuthSchemeName, context.Document);
|
||||
operation.Security ??= new List<OpenApiSecurityRequirement>();
|
||||
operation.Security.Add(new OpenApiSecurityRequirement { [schemaRef] = [] });
|
||||
}
|
||||
|
||||
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
|
||||
{
|
||||
if (context.DocumentName != DeliveryApiConfiguration.ApiName)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
swaggerDoc.AddComponent(
|
||||
AuthSchemeName,
|
||||
new OpenApiSecurityScheme
|
||||
{
|
||||
In = ParameterLocation.Header,
|
||||
Name = AuthSchemeName,
|
||||
Type = SecuritySchemeType.OAuth2,
|
||||
Description = "Umbraco Member Authentication",
|
||||
Flows = new OpenApiOAuthFlows
|
||||
{
|
||||
AuthorizationCode = new OpenApiOAuthFlow
|
||||
{
|
||||
AuthorizationUrl = new Uri(Paths.MemberApi.AuthorizationEndpoint, UriKind.Relative),
|
||||
TokenUrl = new Uri(Paths.MemberApi.TokenEndpoint, UriKind.Relative),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ public abstract class DeliveryApiControllerBase : Controller, IUmbracoFeature
|
||||
{
|
||||
protected string DecodePath(string path)
|
||||
{
|
||||
// OpenAPI does not allow reserved chars as "in:path" parameters, so clients based on the Swagger JSON will URL
|
||||
// OpenAPI does not allow reserved chars as "in:path" parameters, so clients based on the OpenAPI specification will URL
|
||||
// encode the path. Normally, ASP.NET Core handles that encoding with an automatic decoding - apparently just not
|
||||
// for forward slashes, for whatever reason... so we need to deal with those. Hopefully this will be addressed in
|
||||
// an upcoming version of ASP.NET Core.
|
||||
|
||||
@@ -5,7 +5,7 @@ using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Umbraco.Cms.Api.Common.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Delivery.Accessors;
|
||||
@@ -53,7 +53,7 @@ public static class UmbracoBuilderExtensions
|
||||
provider =>
|
||||
{
|
||||
HttpContext? httpContext = provider.GetRequiredService<IHttpContextAccessor>().HttpContext;
|
||||
ApiVersion? apiVersion = httpContext?.GetRequestedApiVersion();
|
||||
ApiVersion? apiVersion = httpContext?.RequestedApiVersion;
|
||||
if (apiVersion is null)
|
||||
{
|
||||
return provider.GetRequiredService<RequestContextOutputExpansionStrategyV2>();
|
||||
@@ -67,7 +67,6 @@ public static class UmbracoBuilderExtensions
|
||||
ServiceLifetime.Scoped);
|
||||
|
||||
builder.Services.AddSingleton<IRequestCultureService, RequestCultureService>();
|
||||
builder.Services.AddSingleton<IRequestSegmmentService, RequestSegmentService>();
|
||||
builder.Services.AddSingleton<IRequestSegmentService, RequestSegmentService>();
|
||||
builder.Services.AddSingleton<IRequestRoutingService, RequestRoutingService>();
|
||||
builder.Services.AddSingleton<IRequestRedirectService, RequestRedirectService>();
|
||||
@@ -86,19 +85,27 @@ public static class UmbracoBuilderExtensions
|
||||
builder.Services.AddTransient<IRequestMemberAccessService, RequestMemberAccessService>();
|
||||
builder.Services.AddTransient<ICurrentMemberClaimsProvider, CurrentMemberClaimsProvider>();
|
||||
|
||||
builder.Services.ConfigureOptions<ConfigureUmbracoDeliveryApiSwaggerGenOptions>();
|
||||
builder.AddUmbracoApiOpenApiUI();
|
||||
builder.AddUmbracoOpenApi();
|
||||
builder.AddUmbracoOpenApiDocument<ConfigureUmbracoDeliveryApiOpenApiOptions>(
|
||||
DeliveryApiConfiguration.ApiName,
|
||||
DeliveryApiConfiguration.ApiTitle,
|
||||
Constants.JsonOptionsNames.DeliveryApi);
|
||||
|
||||
builder
|
||||
.Services
|
||||
.AddControllers()
|
||||
.AddJsonOptions(Constants.JsonOptionsNames.DeliveryApi, options =>
|
||||
{
|
||||
// all Delivery API specific JSON options go here
|
||||
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
|
||||
options.JsonSerializerOptions.TypeInfoResolver = new DeliveryApiJsonTypeResolver();
|
||||
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
|
||||
});
|
||||
.AddJsonOptions(
|
||||
Constants.JsonOptionsNames.DeliveryApi,
|
||||
options =>
|
||||
{
|
||||
// all Delivery API specific JSON options go here
|
||||
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
|
||||
options.JsonSerializerOptions.TypeInfoResolver = new DeliveryApiJsonTypeResolver();
|
||||
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
|
||||
});
|
||||
|
||||
// Configures the JSON options for the Open API schema generation (based on the Delivery API MVC JSON options)
|
||||
builder.Services.ConfigureOptions<ConfigureUmbracoDeliveryHttpJsonOptions>();
|
||||
|
||||
builder.Services.AddAuthentication();
|
||||
builder.AddUmbracoOpenIddict();
|
||||
@@ -157,18 +164,14 @@ public static class UmbracoBuilderExtensions
|
||||
builder.AddNotificationAsyncHandler<ContentCacheRefresherNotification, DeliveryApiDocumentOutputCacheEvictionHandler>();
|
||||
builder.AddNotificationAsyncHandler<MediaCacheRefresherNotification, DeliveryApiMediaOutputCacheEvictionHandler>();
|
||||
builder.AddNotificationAsyncHandler<MemberCacheRefresherNotification, DeliveryApiMemberOutputCacheEvictionHandler>();
|
||||
builder.AddNotificationAsyncHandler<ElementCacheRefresherNotification, DeliveryApiElementOutputCacheEvictionHandler>();
|
||||
|
||||
// Register extension point default implementations.
|
||||
builder.Services.AddSingleton<IDeliveryApiOutputCacheTagProvider, DeliveryApiContentTypeOutputCacheTagProvider>();
|
||||
builder.Services.AddUnique<IDeliveryApiOutputCacheRequestFilter, DefaultDeliveryApiOutputCacheRequestFilter>();
|
||||
builder.Services.AddUnique<IDeliveryApiOutputCacheManager, DeliveryApiOutputCacheManager>();
|
||||
|
||||
// Signal that Umbraco has enabled output caching so the application builder registers
|
||||
// the output cache middleware. Gated via a marker rather than IOutputCacheStore so that
|
||||
// applications calling services.AddOutputCache(...) for their own purposes are not
|
||||
// affected by Umbraco's automatic middleware registration.
|
||||
builder.Services.TryAddSingleton<IUmbracoManagedOutputCacheMarker, UmbracoManagedOutputCacheMarker>();
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.OpenApi;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using Umbraco.Cms.Api.Delivery.Configuration;
|
||||
using Umbraco.Cms.Api.Delivery.Controllers.Content;
|
||||
using Umbraco.Cms.Core;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Filters;
|
||||
|
||||
internal sealed class SwaggerContentDocumentationFilter : SwaggerDocumentationFilterBase<ContentApiControllerBase>
|
||||
{
|
||||
protected override string DocumentationLink => DeliveryApiConfiguration.ApiDocumentationContentArticleLink;
|
||||
|
||||
protected override void ApplyOperation(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
operation.Parameters ??= new List<IOpenApiParameter>();
|
||||
|
||||
AddExpand(operation, context);
|
||||
|
||||
AddFields(operation, context);
|
||||
|
||||
operation.Parameters.Add(new OpenApiParameter
|
||||
{
|
||||
Name = Constants.DeliveryApi.HeaderNames.AcceptLanguage,
|
||||
In = ParameterLocation.Header,
|
||||
Required = false,
|
||||
Description = "Defines the language to return. Use this when querying language variant content items.",
|
||||
Schema = new OpenApiSchema { Type = JsonSchemaType.String },
|
||||
Examples = new Dictionary<string, IOpenApiExample>
|
||||
{
|
||||
{ "Default", new OpenApiExample { Value = string.Empty } },
|
||||
{ "English culture", new OpenApiExample { Value = "en-us" } },
|
||||
},
|
||||
});
|
||||
|
||||
operation.Parameters.Add(new OpenApiParameter
|
||||
{
|
||||
Name = Constants.DeliveryApi.HeaderNames.AcceptSegment,
|
||||
In = ParameterLocation.Header,
|
||||
Required = false,
|
||||
Description = "Defines the segment to return. Use this when querying segment variant content items.",
|
||||
Schema = new OpenApiSchema { Type = JsonSchemaType.String },
|
||||
Examples = new Dictionary<string, IOpenApiExample>
|
||||
{
|
||||
{ "Default", new OpenApiExample { Value = string.Empty } },
|
||||
{ "Segment One", new OpenApiExample { Value = "segment-one" } },
|
||||
},
|
||||
});
|
||||
|
||||
AddApiKey(operation);
|
||||
|
||||
operation.Parameters.Add(new OpenApiParameter
|
||||
{
|
||||
Name = Constants.DeliveryApi.HeaderNames.Preview,
|
||||
In = ParameterLocation.Header,
|
||||
Required = false,
|
||||
Description = "Whether to request draft content.",
|
||||
Schema = new OpenApiSchema { Type = JsonSchemaType.Boolean },
|
||||
});
|
||||
|
||||
operation.Parameters.Add(new OpenApiParameter
|
||||
{
|
||||
Name = Constants.DeliveryApi.HeaderNames.StartItem,
|
||||
In = ParameterLocation.Header,
|
||||
Required = false,
|
||||
Description = "URL segment or GUID of a root content item.",
|
||||
Schema = new OpenApiSchema { Type = JsonSchemaType.String },
|
||||
});
|
||||
}
|
||||
|
||||
protected override void ApplyParameter(OpenApiParameter parameter, ParameterFilterContext context)
|
||||
{
|
||||
switch (parameter.Name)
|
||||
{
|
||||
case "fetch":
|
||||
AddQueryParameterDocumentation(parameter, FetchQueryParameterExamples(), "Specifies the content items to fetch");
|
||||
break;
|
||||
case "filter":
|
||||
AddQueryParameterDocumentation(parameter, FilterQueryParameterExamples(), "Defines how to filter the fetched content items");
|
||||
break;
|
||||
case "sort":
|
||||
AddQueryParameterDocumentation(parameter, SortQueryParameterExamples(), "Defines how to sort the found content items");
|
||||
break;
|
||||
case "skip":
|
||||
parameter.Description = PaginationDescription(true, "content");
|
||||
break;
|
||||
case "take":
|
||||
parameter.Description = PaginationDescription(false, "content");
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private Dictionary<string, IOpenApiExample> FetchQueryParameterExamples() =>
|
||||
new()
|
||||
{
|
||||
{ "Select all", new OpenApiExample { Value = string.Empty } },
|
||||
{ "Select all ancestors of a node by id", new OpenApiExample { Value = "ancestors:id" } },
|
||||
{ "Select all ancestors of a node by path", new OpenApiExample { Value = "ancestors:path" } },
|
||||
{ "Select all children of a node by id", new OpenApiExample { Value = "children:id" } },
|
||||
{ "Select all children of a node by path", new OpenApiExample { Value = "children:path" } },
|
||||
{ "Select all descendants of a node by id", new OpenApiExample { Value = "descendants:id" } },
|
||||
{ "Select all descendants of a node by path", new OpenApiExample { Value = "descendants:path" } },
|
||||
};
|
||||
|
||||
private Dictionary<string, IOpenApiExample> FilterQueryParameterExamples() =>
|
||||
new()
|
||||
{
|
||||
{ "Default filter", new OpenApiExample { Value = string.Empty } },
|
||||
{ "Filter by content type (equals)", new OpenApiExample { Value = new JsonArray { "contentType:alias1" } } },
|
||||
{ "Filter by name (contains)", new OpenApiExample { Value = new JsonArray { "name:nodeName" } } },
|
||||
{ "Filter by creation date (less than)", new OpenApiExample { Value = new JsonArray { "createDate<2024-01-01" } } },
|
||||
{ "Filter by update date (greater than or equal)", new OpenApiExample { Value = new JsonArray { "updateDate>:2023-01-01" } } },
|
||||
};
|
||||
|
||||
private Dictionary<string, IOpenApiExample> SortQueryParameterExamples() =>
|
||||
new()
|
||||
{
|
||||
{ "Default sort", new OpenApiExample { Value = string.Empty } },
|
||||
{ "Sort by create date", new OpenApiExample { Value = new JsonArray { "createDate:asc", "createDate:desc" } } },
|
||||
{ "Sort by level", new OpenApiExample { Value = new JsonArray { "level:asc", "level:desc" } } },
|
||||
{ "Sort by name", new OpenApiExample { Value = new JsonArray { "name:asc", "name:desc" } } },
|
||||
{ "Sort by sort order", new OpenApiExample { Value = new JsonArray { "sortOrder:asc", "sortOrder:desc" } } },
|
||||
{ "Sort by update date", new OpenApiExample { Value = new JsonArray { "updateDate:asc", "updateDate:desc" } } },
|
||||
};
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.OpenApi;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using Umbraco.Cms.Core;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Filters;
|
||||
|
||||
internal abstract class SwaggerDocumentationFilterBase<TBaseController>
|
||||
: SwaggerFilterBase<TBaseController>, IOperationFilter, IParameterFilter
|
||||
where TBaseController : Controller
|
||||
{
|
||||
protected abstract string DocumentationLink { get; }
|
||||
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
if (CanApply(context))
|
||||
{
|
||||
ApplyOperation(operation, context);
|
||||
}
|
||||
}
|
||||
|
||||
public void Apply(IOpenApiParameter parameter, ParameterFilterContext context)
|
||||
{
|
||||
if (CanApply(context) && parameter is OpenApiParameter openApiParameter)
|
||||
{
|
||||
ApplyParameter(openApiParameter, context);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void ApplyOperation(OpenApiOperation operation, OperationFilterContext context);
|
||||
|
||||
protected abstract void ApplyParameter(OpenApiParameter parameter, ParameterFilterContext context);
|
||||
|
||||
protected void AddQueryParameterDocumentation(OpenApiParameter parameter, Dictionary<string, IOpenApiExample> examples, string description)
|
||||
{
|
||||
parameter.Description = QueryParameterDescription(description);
|
||||
parameter.Examples = examples;
|
||||
}
|
||||
|
||||
protected void AddExpand(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
if (IsApiV1(context))
|
||||
{
|
||||
AddExpandV1(operation);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddExpand(operation);
|
||||
}
|
||||
}
|
||||
|
||||
protected void AddFields(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
if (IsApiV1(context))
|
||||
{
|
||||
// "fields" is not a thing in Delivery API V1
|
||||
return;
|
||||
}
|
||||
|
||||
AddFields(operation);
|
||||
}
|
||||
|
||||
protected void AddApiKey(OpenApiOperation operation)
|
||||
{
|
||||
operation.Parameters ??= new List<IOpenApiParameter>();
|
||||
operation.Parameters.Add(
|
||||
new OpenApiParameter
|
||||
{
|
||||
Name = Constants.DeliveryApi.HeaderNames.ApiKey,
|
||||
In = ParameterLocation.Header,
|
||||
Required = false,
|
||||
Description = "API key specified through configuration to authorize access to the API.",
|
||||
Schema = new OpenApiSchema { Type = JsonSchemaType.String },
|
||||
});
|
||||
}
|
||||
|
||||
protected string PaginationDescription(bool skip, string itemType)
|
||||
=> $"Specifies the number of found {itemType} items to {(skip ? "skip" : "take")}. Use this to control pagination of the response.";
|
||||
|
||||
private string QueryParameterDescription(string description)
|
||||
=> $"{description}. Refer to [the documentation]({DocumentationLink}#query-parameters) for more details on this.";
|
||||
|
||||
// FIXME: remove this when Delivery API V1 has been removed (expectedly in V15)
|
||||
private static bool IsApiV1(OperationFilterContext context)
|
||||
=> context.ApiDescription.RelativePath?.Contains("api/v1") is true;
|
||||
|
||||
// FIXME: remove this when Delivery API V1 has been removed (expectedly in V15)
|
||||
private void AddExpandV1(OpenApiOperation operation)
|
||||
{
|
||||
operation.Parameters ??= new List<IOpenApiParameter>();
|
||||
operation.Parameters.Add(
|
||||
new OpenApiParameter
|
||||
{
|
||||
Name = "expand",
|
||||
In = ParameterLocation.Query,
|
||||
Required = false,
|
||||
Description =
|
||||
QueryParameterDescription("Defines the properties that should be expanded in the response"),
|
||||
Schema = new OpenApiSchema { Type = JsonSchemaType.String },
|
||||
Examples = new Dictionary<string, IOpenApiExample>
|
||||
{
|
||||
{ "Expand none", new OpenApiExample { Value = string.Empty } },
|
||||
{ "Expand all", new OpenApiExample { Value = "all" } },
|
||||
{ "Expand specific property", new OpenApiExample { Value = "property:alias1" } },
|
||||
{ "Expand specific properties", new OpenApiExample { Value = "property:alias1,alias2" } },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private void AddExpand(OpenApiOperation operation)
|
||||
{
|
||||
operation.Parameters ??= new List<IOpenApiParameter>();
|
||||
operation.Parameters.Add(
|
||||
new OpenApiParameter
|
||||
{
|
||||
Name = "expand",
|
||||
In = ParameterLocation.Query,
|
||||
Required = false,
|
||||
Description =
|
||||
QueryParameterDescription("Defines the properties that should be expanded in the response"),
|
||||
Schema = new OpenApiSchema { Type = JsonSchemaType.String },
|
||||
Examples = new Dictionary<string, IOpenApiExample>
|
||||
{
|
||||
{ "Expand none", new OpenApiExample { Value = string.Empty } },
|
||||
{ "Expand all properties", new OpenApiExample { Value = "properties[$all]" } },
|
||||
{ "Expand specific property", new OpenApiExample { Value = "properties[alias1]" } },
|
||||
{ "Expand specific properties", new OpenApiExample { Value = "properties[alias1,alias2]" } },
|
||||
{ "Expand nested properties", new OpenApiExample { Value = "properties[alias1[properties[nestedAlias1,nestedAlias2]]]" } },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private void AddFields(OpenApiOperation operation)
|
||||
{
|
||||
operation.Parameters ??= new List<IOpenApiParameter>();
|
||||
operation.Parameters.Add(
|
||||
new OpenApiParameter
|
||||
{
|
||||
Name = "fields",
|
||||
In = ParameterLocation.Query,
|
||||
Required = false,
|
||||
Description =
|
||||
QueryParameterDescription(
|
||||
"Explicitly defines which properties should be included in the response (by default all properties are included)"),
|
||||
Schema = new OpenApiSchema { Type = JsonSchemaType.String },
|
||||
Examples = new Dictionary<string, IOpenApiExample>
|
||||
{
|
||||
{ "Include all properties", new OpenApiExample { Value = "properties[$all]" } },
|
||||
{ "Include only specific property", new OpenApiExample { Value = "properties[alias1]" } },
|
||||
{ "Include only specific properties", new OpenApiExample { Value = "properties[alias1,alias2]" } },
|
||||
{ "Include only specific nested properties", new OpenApiExample { Value = "properties[alias1[properties[nestedAlias1,nestedAlias2]]]" } },
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Filters;
|
||||
|
||||
internal abstract class SwaggerFilterBase<TBaseController>
|
||||
where TBaseController : Controller
|
||||
{
|
||||
protected bool CanApply(OperationFilterContext context)
|
||||
=> CanApply(context.MethodInfo);
|
||||
|
||||
protected bool CanApply(ParameterFilterContext context)
|
||||
=> CanApply(context.ParameterInfo.Member);
|
||||
|
||||
private bool CanApply(MemberInfo member)
|
||||
=> member.DeclaringType?.Implements<TBaseController>() is true;
|
||||
}
|
||||
@@ -51,7 +51,7 @@ public abstract class DeliveryApiVersionAwareJsonConverterBase<T> : JsonConverte
|
||||
private int? GetApiVersion()
|
||||
{
|
||||
HttpContext? httpContext = _httpContextAccessor.HttpContext;
|
||||
ApiVersion? apiVersion = httpContext?.GetRequestedApiVersion();
|
||||
ApiVersion? apiVersion = httpContext?.RequestedApiVersion;
|
||||
|
||||
return apiVersion?.MajorVersion;
|
||||
}
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
using Microsoft.AspNetCore.OpenApi;
|
||||
using Microsoft.OpenApi;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.OpenApi.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for <see cref="OpenApiSchemaTransformerContext"/>.
|
||||
/// </summary>
|
||||
internal static class OpenApiSchemaTransformerContextExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the OpenAPI document from the context, throwing if it is null.
|
||||
/// </summary>
|
||||
/// <param name="context">The schema transformer context.</param>
|
||||
/// <returns>The OpenAPI document.</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown when the document is null.</exception>
|
||||
public static OpenApiDocument GetRequiredDocument(this OpenApiSchemaTransformerContext context)
|
||||
=> context.Document ?? throw new InvalidOperationException("OpenAPI document context is required for schema registration.");
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using Microsoft.AspNetCore.OpenApi;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Delivery.Configuration;
|
||||
using Umbraco.Cms.Api.Delivery.OpenApi.Transformers;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for configuring OpenAPI support for the Delivery API.
|
||||
/// </summary>
|
||||
public static class OpenApiServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds member authentication support to the Delivery API OpenAPI document.
|
||||
/// </summary>
|
||||
/// <param name="services">The <see cref="IServiceCollection"/> to configure.</param>
|
||||
/// <returns>The configured <see cref="IServiceCollection"/> instance.</returns>
|
||||
/// <remarks>
|
||||
/// This enables the OAuth2 authorization code flow for member authentication in Swagger UI.
|
||||
/// Consult the Delivery API member authentication documentation for setup instructions.
|
||||
/// </remarks>
|
||||
public static IServiceCollection AddDeliveryApiOpenApiMemberAuthentication(this IServiceCollection services)
|
||||
{
|
||||
services.PostConfigure<OpenApiOptions>(
|
||||
DeliveryApiConfiguration.ApiName,
|
||||
options =>
|
||||
{
|
||||
options.AddDocumentTransformer<MemberAuthenticationSecurityRequirementsTransformer>();
|
||||
options.AddOperationTransformer<MemberAuthenticationSecurityRequirementsTransformer>();
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using Microsoft.AspNetCore.OpenApi;
|
||||
using Microsoft.OpenApi;
|
||||
using Umbraco.Cms.Core;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.OpenApi.Transformers;
|
||||
|
||||
/// <summary>
|
||||
/// Transforms the OpenAPI document to include API key security scheme.
|
||||
/// </summary>
|
||||
internal class ApiKeyTransformer : IOpenApiDocumentTransformer, IOpenApiOperationTransformer
|
||||
{
|
||||
private const string AuthSchemeName = "ApiKeyAuth";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task TransformAsync(
|
||||
OpenApiDocument document,
|
||||
OpenApiDocumentTransformerContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var apiKeyScheme = new OpenApiSecurityScheme
|
||||
{
|
||||
Type = SecuritySchemeType.ApiKey,
|
||||
Name = Constants.DeliveryApi.HeaderNames.ApiKey,
|
||||
In = ParameterLocation.Header,
|
||||
Description = "API key specified through configuration to authorize access to the API.",
|
||||
};
|
||||
|
||||
document.Components ??= new OpenApiComponents();
|
||||
document.Components.SecuritySchemes ??= new Dictionary<string, IOpenApiSecurityScheme>();
|
||||
document.Components.SecuritySchemes[AuthSchemeName] = apiKeyScheme;
|
||||
|
||||
var schemaRef = new OpenApiSecuritySchemeReference(AuthSchemeName, document);
|
||||
document.Security ??= new List<OpenApiSecurityRequirement>();
|
||||
document.Security.Add(new OpenApiSecurityRequirement { [schemaRef] = [] });
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task TransformAsync(
|
||||
OpenApiOperation operation,
|
||||
OpenApiOperationTransformerContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var schemaRef = new OpenApiSecuritySchemeReference(AuthSchemeName, context.Document);
|
||||
operation.Security ??= new List<OpenApiSecurityRequirement>();
|
||||
operation.Security.Add(new OpenApiSecurityRequirement { [schemaRef] = [] });
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.AspNetCore.OpenApi;
|
||||
using Microsoft.OpenApi;
|
||||
using Umbraco.Cms.Api.Delivery.Configuration;
|
||||
using Umbraco.Cms.Api.Delivery.Controllers.Content;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.OpenApi.Transformers;
|
||||
|
||||
/// <summary>
|
||||
/// Transforms OpenAPI operations for the Content API, adding relevant parameters and documentation.
|
||||
/// </summary>
|
||||
internal sealed class ContentApiTransformer : DeliveryApiTransformerBase
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
protected override string DocumentationLink => DeliveryApiConfiguration.ApiDocumentationContentArticleLink;
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool ShouldApply(OpenApiOperationTransformerContext context) =>
|
||||
context.Description.ActionDescriptor is ControllerActionDescriptor description
|
||||
&& description.ControllerTypeInfo.Implements<ContentApiControllerBase>();
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Task ApplyAsync(
|
||||
OpenApiOperation operation,
|
||||
OpenApiOperationTransformerContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
operation.Parameters ??= new List<IOpenApiParameter>();
|
||||
operation.Parameters.Add(
|
||||
new OpenApiParameter
|
||||
{
|
||||
Name = Constants.DeliveryApi.HeaderNames.AcceptLanguage,
|
||||
In = ParameterLocation.Header,
|
||||
Required = false,
|
||||
Description = "Defines the language to return. Use this when querying language variant content items.",
|
||||
Schema = new OpenApiSchema { Type = JsonSchemaType.String },
|
||||
Examples = new Dictionary<string, IOpenApiExample>
|
||||
{
|
||||
{ "Default", new OpenApiExample { Value = string.Empty } },
|
||||
{ "English culture", new OpenApiExample { Value = "en-us" } },
|
||||
},
|
||||
});
|
||||
|
||||
operation.Parameters.Add(
|
||||
new OpenApiParameter
|
||||
{
|
||||
Name = Constants.DeliveryApi.HeaderNames.AcceptSegment,
|
||||
In = ParameterLocation.Header,
|
||||
Required = false,
|
||||
Description = "Defines the segment to return. Use this when querying segment variant content items.",
|
||||
Schema = new OpenApiSchema { Type = JsonSchemaType.String },
|
||||
Examples = new Dictionary<string, IOpenApiExample>
|
||||
{
|
||||
{ "Default", new OpenApiExample { Value = string.Empty } },
|
||||
{ "Segment One", new OpenApiExample { Value = "segment-one" } },
|
||||
},
|
||||
});
|
||||
|
||||
operation.Parameters.Add(
|
||||
new OpenApiParameter
|
||||
{
|
||||
Name = Constants.DeliveryApi.HeaderNames.Preview,
|
||||
In = ParameterLocation.Header,
|
||||
Required = false,
|
||||
Description = "Whether to request draft content.",
|
||||
Schema = new OpenApiSchema { Type = JsonSchemaType.Boolean },
|
||||
});
|
||||
|
||||
operation.Parameters.Add(
|
||||
new OpenApiParameter
|
||||
{
|
||||
Name = Constants.DeliveryApi.HeaderNames.StartItem,
|
||||
In = ParameterLocation.Header,
|
||||
Required = false,
|
||||
Description = "URL segment or GUID of a root content item.",
|
||||
Schema = new OpenApiSchema { Type = JsonSchemaType.String },
|
||||
});
|
||||
|
||||
foreach (OpenApiParameter parameter in operation.Parameters?.OfType<OpenApiParameter>() ?? [])
|
||||
{
|
||||
ApplyParameter(parameter);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void ApplyParameter(OpenApiParameter parameter)
|
||||
{
|
||||
switch (parameter.Name)
|
||||
{
|
||||
case "fetch":
|
||||
AddQueryParameterDocumentation(parameter, FetchQueryParameterExamples(), "Specifies the content items to fetch");
|
||||
break;
|
||||
case "filter":
|
||||
AddQueryParameterDocumentation(parameter, FilterQueryParameterExamples(), "Defines how to filter the fetched content items");
|
||||
break;
|
||||
case "sort":
|
||||
AddQueryParameterDocumentation(parameter, SortQueryParameterExamples(), "Defines how to sort the found content items");
|
||||
break;
|
||||
case "skip":
|
||||
parameter.Description = PaginationDescription(true, "content");
|
||||
break;
|
||||
case "take":
|
||||
parameter.Description = PaginationDescription(false, "content");
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private Dictionary<string, IOpenApiExample> FetchQueryParameterExamples() =>
|
||||
new()
|
||||
{
|
||||
{ "Select all", new OpenApiExample { Value = "" } },
|
||||
{ "Select all ancestors of a node by id", new OpenApiExample { Value = "ancestors:id" } },
|
||||
{ "Select all ancestors of a node by path", new OpenApiExample { Value = "ancestors:path" } },
|
||||
{ "Select all children of a node by id", new OpenApiExample { Value = "children:id" } },
|
||||
{ "Select all children of a node by path", new OpenApiExample { Value = "children:path" } },
|
||||
{ "Select all descendants of a node by id", new OpenApiExample { Value = "descendants:id" } },
|
||||
{ "Select all descendants of a node by path", new OpenApiExample { Value = "descendants:path" } },
|
||||
};
|
||||
|
||||
private Dictionary<string, IOpenApiExample> FilterQueryParameterExamples() =>
|
||||
new()
|
||||
{
|
||||
{ "Default filter", new OpenApiExample { Value = new JsonArray("") } },
|
||||
{ "Filter by content type (equals)", new OpenApiExample { Value = new JsonArray("contentType:alias1") } },
|
||||
{ "Filter by name (contains)", new OpenApiExample { Value = new JsonArray("name:nodeName") } },
|
||||
{ "Filter by creation date (less than)", new OpenApiExample { Value = new JsonArray("createDate<2024-01-01") } },
|
||||
{ "Filter by update date (greater than or equal)", new OpenApiExample { Value = new JsonArray("updateDate>:2023-01-01") } },
|
||||
};
|
||||
|
||||
private Dictionary<string, IOpenApiExample> SortQueryParameterExamples() =>
|
||||
new()
|
||||
{
|
||||
{ "Default sort", new OpenApiExample { Value = new JsonArray("") } },
|
||||
{ "Sort by create date", new OpenApiExample { Value = new JsonArray("createDate:asc", "createDate:desc") } },
|
||||
{ "Sort by level", new OpenApiExample { Value = new JsonArray("level:asc", "level:desc") } },
|
||||
{ "Sort by name", new OpenApiExample { Value = new JsonArray("name:asc", "name:desc") } },
|
||||
{ "Sort by sort order", new OpenApiExample { Value = new JsonArray("sortOrder:asc", "sortOrder:desc") } },
|
||||
{ "Sort by update date", new OpenApiExample { Value = new JsonArray("updateDate:asc", "updateDate:desc") } },
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,691 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
using Microsoft.AspNetCore.Http.Json;
|
||||
using Microsoft.AspNetCore.OpenApi;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.OpenApi;
|
||||
using Umbraco.Cms.Api.Common.Configuration;
|
||||
using Umbraco.Cms.Api.Delivery.OpenApi.Extensions;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.OpenApi.Transformers;
|
||||
|
||||
/// <summary>
|
||||
/// Transforms the OpenAPI document to add schemas for the instance's document types.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This transformer implements both <see cref="IOpenApiSchemaTransformer"/> and <see cref="IOpenApiDocumentTransformer"/>
|
||||
/// to handle schema generation in two phases:
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Phase 1 - Schema Transformation:</b> When the schema transformer encounters types like
|
||||
/// <see cref="IApiContentResponse"/> or <see cref="IApiMediaWithCrops"/>, it generates content-type-specific
|
||||
/// schemas (e.g., "ArticleContentResponseModel") and registers them as components in the OpenAPI document.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Circular Reference Handling:</b> Content type schemas can reference each other (e.g., a "Page"
|
||||
/// might have a property of type "Article", which might reference "Page" again). To prevent infinite recursion
|
||||
/// during schema generation, we use a placeholder pattern:
|
||||
/// <list type="bullet">
|
||||
/// <item>When generating a schema, we track its ID in <c>_handledSchemas</c></item>
|
||||
/// <item>If we encounter the same schema ID again (circular reference), we return a temporary placeholder
|
||||
/// schema with metadata marking it for later replacement</item>
|
||||
/// <item>The placeholder contains a <c>x-recursive-ref</c> metadata key with the target schema ID</item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Phase 2 - Document Transformation:</b> After all schemas are generated, the document transformer
|
||||
/// resolves inline schemas into proper <c>$ref</c> references. This handles two cases:
|
||||
/// <list type="bullet">
|
||||
/// <item>Circular reference placeholders (marked with <c>x-recursive-ref</c>) created during Phase 1</item>
|
||||
/// <item>Componentized schemas (marked with <c>x-schema-id</c>) that the framework did not automatically
|
||||
/// resolve to <c>$ref</c> — this can happen for schemas reached through properties or composition
|
||||
/// rather than as direct API response types</item>
|
||||
/// </list>
|
||||
/// This is done by <see cref="ResolveSchemaReferences(OpenApiDocument, IOpenApiSchema)"/> which recursively walks
|
||||
/// through all schemas and substitutes matching entries with <see cref="OpenApiSchemaReference"/> instances.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class ContentTypeSchemaTransformer : IOpenApiSchemaTransformer, IOpenApiDocumentTransformer
|
||||
{
|
||||
// Metadata keys
|
||||
private const string RecursiveRefMetadataKey = "x-recursive-ref";
|
||||
private const string SchemaIdMetadataKey = "x-schema-id";
|
||||
|
||||
// Schema ID suffixes
|
||||
private const string ResponseModelSuffix = "ResponseModel";
|
||||
private const string ModelSuffix = "Model";
|
||||
private const string ContentSuffix = "Content";
|
||||
private const string ElementSuffix = "Element";
|
||||
private const string MediaSuffix = "Media";
|
||||
private const string MediaWithCropsSuffix = "MediaWithCrops";
|
||||
private const string PropertiesModelSuffix = "PropertiesModel";
|
||||
|
||||
private readonly IContentTypeSchemaService _contentTypeSchemaService;
|
||||
private readonly IOptionsMonitor<DeliveryApiSettings> _deliveryApiSettings;
|
||||
private readonly ILogger<ContentTypeSchemaTransformer> _logger;
|
||||
private readonly IJsonTypeInfoResolver _jsonTypeInfoResolver;
|
||||
|
||||
/// <summary>
|
||||
/// Tracks schema IDs that have been or are being generated to detect circular references.
|
||||
/// When a schema ID is encountered a second time, a placeholder is returned instead of recursing infinitely.
|
||||
/// </summary>
|
||||
private readonly HashSet<string> _handledSchemas = [];
|
||||
private readonly JsonSerializerOptions _serializerOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ContentTypeSchemaTransformer"/> class.
|
||||
/// </summary>
|
||||
/// <param name="contentTypeSchemaService">The content type info service.</param>
|
||||
/// <param name="jsonOptionsMonitor">The JSON options monitor.</param>
|
||||
/// <param name="deliveryApiSettings">The Delivery API settings, used to honour the allow/deny content type list.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public ContentTypeSchemaTransformer(
|
||||
IContentTypeSchemaService contentTypeSchemaService,
|
||||
IOptionsMonitor<JsonOptions> jsonOptionsMonitor,
|
||||
IOptionsMonitor<DeliveryApiSettings> deliveryApiSettings,
|
||||
ILogger<ContentTypeSchemaTransformer> logger)
|
||||
{
|
||||
_contentTypeSchemaService = contentTypeSchemaService;
|
||||
_deliveryApiSettings = deliveryApiSettings;
|
||||
_logger = logger;
|
||||
_serializerOptions = jsonOptionsMonitor
|
||||
.Get(Constants.JsonOptionsNames.DeliveryApi)
|
||||
.SerializerOptions;
|
||||
_jsonTypeInfoResolver = _serializerOptions.TypeInfoResolver
|
||||
?? throw new InvalidOperationException("The JSON serializer options must have a TypeInfoResolver configured.");
|
||||
}
|
||||
|
||||
private IReadOnlyCollection<ContentTypeSchemaInfo> DocumentTypes
|
||||
=> field ??= FilterAllowedDocumentTypes(_contentTypeSchemaService.GetDocumentTypes());
|
||||
|
||||
private IReadOnlyCollection<ContentTypeSchemaInfo> MediaTypes
|
||||
=> field ??= _contentTypeSchemaService.GetMediaTypes();
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task TransformAsync(
|
||||
OpenApiDocument document,
|
||||
OpenApiDocumentTransformerContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (document.Components?.Schemas is not { Count: > 0 })
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
foreach (IOpenApiSchema componentsSchema in document.Components.Schemas.Values)
|
||||
{
|
||||
ResolveSchemaReferences(document, componentsSchema);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task TransformAsync(
|
||||
OpenApiSchema schema,
|
||||
OpenApiSchemaTransformerContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
switch (context.JsonTypeInfo.Type)
|
||||
{
|
||||
case var type when type == typeof(IApiContentResponse):
|
||||
await ApplyPolymorphicContentType(
|
||||
schema,
|
||||
context,
|
||||
PublishedItemType.Content,
|
||||
DocumentTypes.Where(c => !c.IsElement),
|
||||
async (contentType, derivedTypeSchemas) =>
|
||||
{
|
||||
var schemaIdPrefix = $"{contentType.SchemaId}{ContentSuffix}";
|
||||
return await CreateContentTypeResponseSchema(
|
||||
schemaIdPrefix,
|
||||
derivedTypeSchemas,
|
||||
context);
|
||||
},
|
||||
cancellationToken);
|
||||
await CreateSchema(GetJsonTypeInfo(typeof(IApiContent)), context, cancellationToken);
|
||||
return;
|
||||
case var type when type == typeof(IApiContent):
|
||||
await ApplyPolymorphicContentType(
|
||||
schema,
|
||||
context,
|
||||
PublishedItemType.Content,
|
||||
DocumentTypes.Where(c => !c.IsElement),
|
||||
async (contentType, derivedTypeSchemas) =>
|
||||
{
|
||||
var schemaId = $"{contentType.SchemaId}{ContentSuffix}{ModelSuffix}";
|
||||
return await CreateContentTypeSchema(
|
||||
schemaId,
|
||||
PublishedItemType.Content,
|
||||
contentType,
|
||||
derivedTypeSchemas,
|
||||
context,
|
||||
cancellationToken);
|
||||
},
|
||||
cancellationToken);
|
||||
await CreateSchema(GetJsonTypeInfo(typeof(IApiElement)), context, cancellationToken);
|
||||
return;
|
||||
case var type when type == typeof(IApiElement):
|
||||
await ApplyPolymorphicContentType(
|
||||
schema,
|
||||
context,
|
||||
PublishedItemType.Content,
|
||||
DocumentTypes.Where(c => c.IsElement),
|
||||
async (contentType, derivedTypeSchemas) =>
|
||||
{
|
||||
var schemaId = $"{contentType.SchemaId}{ElementSuffix}{ModelSuffix}";
|
||||
return await CreateContentTypeSchema(
|
||||
schemaId,
|
||||
PublishedItemType.Content,
|
||||
contentType,
|
||||
derivedTypeSchemas,
|
||||
context,
|
||||
cancellationToken);
|
||||
},
|
||||
cancellationToken);
|
||||
return;
|
||||
case var type when type == typeof(IApiMediaWithCropsResponse):
|
||||
await ApplyPolymorphicContentType(
|
||||
schema,
|
||||
context,
|
||||
PublishedItemType.Media,
|
||||
MediaTypes,
|
||||
async (contentType, derivedTypeSchemas) =>
|
||||
{
|
||||
var schemaId = $"{contentType.SchemaId}{MediaWithCropsSuffix}";
|
||||
return await CreateContentTypeResponseSchema(
|
||||
schemaId,
|
||||
derivedTypeSchemas,
|
||||
context);
|
||||
},
|
||||
cancellationToken);
|
||||
await CreateSchema(GetJsonTypeInfo(typeof(IApiMediaWithCrops)), context, cancellationToken);
|
||||
return;
|
||||
case var type when type == typeof(IApiMediaWithCrops):
|
||||
await ApplyPolymorphicContentType(
|
||||
schema,
|
||||
context,
|
||||
PublishedItemType.Media,
|
||||
MediaTypes,
|
||||
async (contentType, derivedTypeSchemas) =>
|
||||
{
|
||||
var schemaId = $"{contentType.SchemaId}{MediaWithCropsSuffix}{ModelSuffix}";
|
||||
return await CreateContentTypeSchema(
|
||||
schemaId,
|
||||
PublishedItemType.Media,
|
||||
contentType,
|
||||
derivedTypeSchemas,
|
||||
context,
|
||||
cancellationToken);
|
||||
},
|
||||
cancellationToken);
|
||||
return;
|
||||
default:
|
||||
// HACK: Some types with circular references (e.g. ApiBlockGridItem) get left
|
||||
// inlined by the framework, breaking $ref resolution. Register them explicitly.
|
||||
if (GetSchemaId(context.JsonTypeInfo) is not { } schemaId || !_handledSchemas.Add(schemaId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
OpenApiDocument document = context.GetRequiredDocument();
|
||||
document.AddComponent(schemaId, schema);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ApplyPolymorphicContentType(
|
||||
OpenApiSchema schema,
|
||||
OpenApiSchemaTransformerContext context,
|
||||
PublishedItemType itemType,
|
||||
IEnumerable<ContentTypeSchemaInfo> contentTypes,
|
||||
Func<ContentTypeSchemaInfo, List<IOpenApiSchema>, Task<OpenApiSchema>> contentTypeSchemaFactory,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
List<IOpenApiSchema> derivedTypeSchemas = await ResolveDerivedTypeSchemas(
|
||||
schema,
|
||||
context,
|
||||
cancellationToken);
|
||||
|
||||
OpenApiDocument document = context.GetRequiredDocument();
|
||||
var typePropertyName = GetTypePropertyName(itemType);
|
||||
schema.Discriminator = new OpenApiDiscriminator
|
||||
{
|
||||
PropertyName = typePropertyName,
|
||||
Mapping = new Dictionary<string, OpenApiSchemaReference>(),
|
||||
};
|
||||
schema.OneOf ??= new List<IOpenApiSchema>();
|
||||
|
||||
foreach (ContentTypeSchemaInfo contentType in contentTypes)
|
||||
{
|
||||
OpenApiSchema contentTypeSchema = await contentTypeSchemaFactory(contentType, derivedTypeSchemas);
|
||||
var schemaId = (string)contentTypeSchema.Metadata![SchemaIdMetadataKey];
|
||||
schema.Discriminator.Mapping[contentType.Alias] = new OpenApiSchemaReference(schemaId, document);
|
||||
schema.OneOf.Add(contentTypeSchema);
|
||||
}
|
||||
|
||||
// Remove all schema properties that are now handled by the derived types
|
||||
schema.AnyOf = null;
|
||||
schema.Properties = null;
|
||||
schema.Required = new HashSet<string> { typePropertyName };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates and adds a schema to the OpenAPI document if it does not already exist.
|
||||
/// </summary>
|
||||
/// <remarks>A placeholder schema is added first to avoid recursion issues when generating schemas that reference themselves.</remarks>
|
||||
private async Task<IOpenApiSchema> CreateSchema(
|
||||
JsonTypeInfo jsonTypeInfo,
|
||||
OpenApiSchemaTransformerContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (jsonTypeInfo.Type.IsArray || jsonTypeInfo.Kind == JsonTypeInfoKind.Enumerable)
|
||||
{
|
||||
Type elementType = jsonTypeInfo.ElementType ?? jsonTypeInfo.Type.GetElementType() ?? typeof(object);
|
||||
JsonTypeInfo elementJsonTypeInfo = GetJsonTypeInfo(elementType);
|
||||
IOpenApiSchema itemSchema = await CreateSchema(elementJsonTypeInfo, context, cancellationToken);
|
||||
return new OpenApiSchema
|
||||
{
|
||||
Type = JsonSchemaType.Array,
|
||||
Items = itemSchema,
|
||||
};
|
||||
}
|
||||
|
||||
var schemaId = GetSchemaId(jsonTypeInfo);
|
||||
|
||||
// If this is one of the types we handle, and we already started generating it, return a placeholder
|
||||
// to avoid circular reference issues.
|
||||
// In the document transformer, these placeholders will be replaced with the actual schemas.
|
||||
if (schemaId is not null && !_handledSchemas.Add(schemaId))
|
||||
{
|
||||
return GetPlaceholderSchema(schemaId);
|
||||
}
|
||||
|
||||
OpenApiSchema schema;
|
||||
try
|
||||
{
|
||||
schema = await context.GetOrCreateSchemaAsync(
|
||||
jsonTypeInfo.Type,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Log the error but continue with a fallback schema to avoid failing the entire document generation.
|
||||
// The fallback schema includes a description indicating the failure, making it visible to API consumers.
|
||||
_logger.LogError(ex, "Failed to create OpenAPI schema for type {TypeName}", jsonTypeInfo.Type.FullName);
|
||||
schema = new OpenApiSchema
|
||||
{
|
||||
Description = $"[Schema generation failed for type '{jsonTypeInfo.Type.FullName}'. See server logs for details.]",
|
||||
};
|
||||
}
|
||||
|
||||
if (schemaId is null)
|
||||
{
|
||||
return schema;
|
||||
}
|
||||
|
||||
OpenApiDocument document = context.GetRequiredDocument();
|
||||
document.AddComponent(schemaId, schema);
|
||||
return new OpenApiSchemaReference(schemaId, document);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allows null at a property reference site without mutating any shared component schema.
|
||||
/// Inline schemas have <c>null</c> OR-ed into their <c>type</c> flags; schema references and
|
||||
/// recursive-ref placeholders are wrapped in a <c>oneOf</c> with an explicit null branch so the
|
||||
/// shared component is left unchanged.
|
||||
/// </summary>
|
||||
private static IOpenApiSchema AsNullable(IOpenApiSchema schema)
|
||||
{
|
||||
if (schema is OpenApiSchema inline
|
||||
&& inline.Metadata?.ContainsKey(RecursiveRefMetadataKey) is not true)
|
||||
{
|
||||
inline.Type |= JsonSchemaType.Null;
|
||||
return inline;
|
||||
}
|
||||
|
||||
return new OpenApiSchema
|
||||
{
|
||||
OneOf =
|
||||
[
|
||||
schema,
|
||||
new OpenApiSchema { Type = JsonSchemaType.Null },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
private static Task<OpenApiSchema> CreateContentTypeResponseSchema(
|
||||
string schemaIdPrefix,
|
||||
List<IOpenApiSchema> derivedTypeSchemas,
|
||||
OpenApiSchemaTransformerContext context)
|
||||
{
|
||||
var schemaId = $"{schemaIdPrefix}{ResponseModelSuffix}";
|
||||
OpenApiDocument document = context.GetRequiredDocument();
|
||||
var schema = new OpenApiSchema
|
||||
{
|
||||
Type = JsonSchemaType.Object,
|
||||
AllOf = [..derivedTypeSchemas, new OpenApiSchemaReference($"{schemaIdPrefix}{ModelSuffix}", document)],
|
||||
Metadata = new Dictionary<string, object> { [SchemaIdMetadataKey] = schemaId },
|
||||
};
|
||||
|
||||
document.AddComponent(schemaId, schema);
|
||||
return Task.FromResult(schema);
|
||||
}
|
||||
|
||||
private async Task<OpenApiSchema> CreateContentTypeSchema(
|
||||
string schemaId,
|
||||
PublishedItemType itemType,
|
||||
ContentTypeSchemaInfo contentType,
|
||||
List<IOpenApiSchema> derivedTypeSchemas,
|
||||
OpenApiSchemaTransformerContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var typePropertyName = GetTypePropertyName(itemType);
|
||||
var schema = new OpenApiSchema
|
||||
{
|
||||
Type = JsonSchemaType.Object,
|
||||
Properties = new Dictionary<string, IOpenApiSchema>
|
||||
{
|
||||
[typePropertyName] = new OpenApiSchema { Const = contentType.Alias },
|
||||
["properties"] = await CreatePropertiesSchema(contentType, itemType, context, cancellationToken),
|
||||
},
|
||||
Required = new HashSet<string> { typePropertyName },
|
||||
AllOf = derivedTypeSchemas.Count > 0 ? derivedTypeSchemas : null,
|
||||
Metadata = new Dictionary<string, object> { [SchemaIdMetadataKey] = schemaId, },
|
||||
};
|
||||
|
||||
OpenApiDocument document = context.GetRequiredDocument();
|
||||
document.AddComponent(schemaId, schema);
|
||||
return schema;
|
||||
}
|
||||
|
||||
private async Task<OpenApiSchemaReference> CreatePropertiesSchema(
|
||||
ContentTypeSchemaInfo contentType,
|
||||
PublishedItemType itemType,
|
||||
OpenApiSchemaTransformerContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var schemaId = GetPropertiesModelSchemaId(contentType, itemType);
|
||||
|
||||
var propertiesSchema = new OpenApiSchema
|
||||
{
|
||||
Type = JsonSchemaType.Object,
|
||||
AllOf =
|
||||
[
|
||||
..contentType.CompositionSchemaIds.Select(compositionSchemaId
|
||||
=> GetPlaceholderSchema(GetCompositionPropertiesModelSchemaId(compositionSchemaId, itemType)))
|
||||
],
|
||||
Properties = await CreateContentTypeProperties(contentType, context, cancellationToken),
|
||||
Metadata = new Dictionary<string, object> { [SchemaIdMetadataKey] = schemaId },
|
||||
};
|
||||
|
||||
OpenApiDocument document = context.GetRequiredDocument();
|
||||
document.AddComponent(schemaId, propertiesSchema);
|
||||
return new OpenApiSchemaReference(schemaId, document);
|
||||
}
|
||||
|
||||
private static string GetPropertiesModelSchemaId(ContentTypeSchemaInfo contentType, PublishedItemType itemType) =>
|
||||
$"{contentType.SchemaId}{GetItemTypeSuffix(itemType, contentType.IsElement)}{PropertiesModelSuffix}";
|
||||
|
||||
private string GetCompositionPropertiesModelSchemaId(string compositionSchemaId, PublishedItemType itemType)
|
||||
{
|
||||
// Look up the composition's own IsElement so its reference points at the right
|
||||
// generated schema (element-type compositions live under the Element suffix).
|
||||
IReadOnlyCollection<ContentTypeSchemaInfo> candidates = itemType == PublishedItemType.Media ? MediaTypes : DocumentTypes;
|
||||
ContentTypeSchemaInfo? composition = candidates.FirstOrDefault(c => c.SchemaId == compositionSchemaId);
|
||||
var suffix = GetItemTypeSuffix(itemType, composition?.IsElement ?? false);
|
||||
return $"{compositionSchemaId}{suffix}{PropertiesModelSuffix}";
|
||||
}
|
||||
|
||||
private static string GetItemTypeSuffix(PublishedItemType itemType, bool isElement) =>
|
||||
itemType switch
|
||||
{
|
||||
PublishedItemType.Media => MediaSuffix,
|
||||
PublishedItemType.Content => isElement ? ElementSuffix : ContentSuffix,
|
||||
_ => throw new NotSupportedException($"Unsupported PublishedItemType: {itemType}"),
|
||||
};
|
||||
|
||||
private async Task<Dictionary<string, IOpenApiSchema>> CreateContentTypeProperties(
|
||||
ContentTypeSchemaInfo contentType,
|
||||
OpenApiSchemaTransformerContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var properties = new Dictionary<string, IOpenApiSchema>();
|
||||
foreach (ContentTypePropertySchemaInfo propertyInfo in contentType.Properties.Where(p => !p.Inherited))
|
||||
{
|
||||
IOpenApiSchema schema = await CreateSchema(
|
||||
GetJsonTypeInfo(propertyInfo.DeliveryApiClrType),
|
||||
context,
|
||||
cancellationToken);
|
||||
|
||||
// Properties may be null (e.g. property added after content was last published).
|
||||
// Nullability is applied at the reference site, never on a shared component schema.
|
||||
properties[propertyInfo.Alias] = AsNullable(schema);
|
||||
}
|
||||
|
||||
return properties;
|
||||
}
|
||||
|
||||
private JsonTypeInfo GetJsonTypeInfo(Type type)
|
||||
{
|
||||
JsonTypeInfo? jsonTypeInfo = _jsonTypeInfoResolver.GetTypeInfo(type, _serializerOptions);
|
||||
return jsonTypeInfo ?? throw new InvalidOperationException("Could not get JsonTypeInfo for type " + type.FullName);
|
||||
}
|
||||
|
||||
private string GetTypePropertyName(PublishedItemType itemType)
|
||||
{
|
||||
var propertyName = itemType switch
|
||||
{
|
||||
PublishedItemType.Content => nameof(IApiElement.ContentType),
|
||||
PublishedItemType.Media => nameof(IApiMedia.MediaType),
|
||||
_ => throw new NotSupportedException($"Unsupported PublishedItemType: {itemType}"),
|
||||
};
|
||||
|
||||
return _serializerOptions.PropertyNamingPolicy?.ConvertName(propertyName) ?? propertyName;
|
||||
}
|
||||
|
||||
private static string? GetSchemaId(JsonTypeInfo type)
|
||||
=> ConfigureUmbracoOpenApiOptionsBase.CreateSchemaReferenceId(type);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a temporary placeholder schema to break circular reference chains during schema generation.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The placeholder contains metadata with the target schema ID. During the document transformation phase,
|
||||
/// <see cref="ResolveSchemaReferences(OpenApiDocument, IOpenApiSchema)"/> will replace these placeholders with actual schema references.
|
||||
/// </remarks>
|
||||
/// <param name="schemaId">The ID of the schema this placeholder represents.</param>
|
||||
/// <returns>A placeholder schema with metadata indicating the target schema reference.</returns>
|
||||
private static OpenApiSchema GetPlaceholderSchema(string schemaId)
|
||||
=> new()
|
||||
{
|
||||
Metadata = new Dictionary<string, object>
|
||||
{
|
||||
[RecursiveRefMetadataKey] = schemaId,
|
||||
},
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Recursively resolves inline schemas into proper <c>$ref</c> references.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method is called during the document transformation phase (after all schemas have been generated).
|
||||
/// It walks through all schema properties, allOf, oneOf, and anyOf collections, resolving two types of
|
||||
/// inline schemas:
|
||||
/// <list type="bullet">
|
||||
/// <item>Circular reference placeholders created by <see cref="GetPlaceholderSchema"/> (marked with <c>x-recursive-ref</c>)</item>
|
||||
/// <item>Componentized schemas that should be references (marked with <c>x-schema-id</c>)</item>
|
||||
/// </list>
|
||||
/// Each match is replaced with an <see cref="OpenApiSchemaReference"/> pointing to the actual schema in the document's components.
|
||||
/// </remarks>
|
||||
/// <param name="document">The OpenAPI document containing the registered schema components.</param>
|
||||
/// <param name="schema">The schema to process (will be modified in place).</param>
|
||||
private static void ResolveSchemaReferences(OpenApiDocument document, IOpenApiSchema schema)
|
||||
{
|
||||
// Replace in allOf, oneOf, anyOf
|
||||
ResolveSchemaReferences(document, schema.AllOf);
|
||||
ResolveSchemaReferences(document, schema.OneOf);
|
||||
ResolveSchemaReferences(document, schema.AnyOf);
|
||||
|
||||
// Process array items
|
||||
if (schema is OpenApiSchema { Items: OpenApiSchema itemsSchema } parentSchema)
|
||||
{
|
||||
parentSchema.Items = GetActualSchemaOrReference(document, itemsSchema, out var itemsReplaced);
|
||||
if (!itemsReplaced)
|
||||
{
|
||||
ResolveSchemaReferences(document, itemsSchema);
|
||||
}
|
||||
}
|
||||
|
||||
if (schema.Properties is not { Count: > 0 })
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Process properties
|
||||
foreach (var propertyKey in schema.Properties.Keys)
|
||||
{
|
||||
IOpenApiSchema propertySchema = schema.Properties[propertyKey];
|
||||
if (propertySchema is not OpenApiSchema innerSchema)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
schema.Properties[propertyKey] = GetActualSchemaOrReference(document, innerSchema, out var replaced);
|
||||
if (replaced)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Recursive call to handle the property schema
|
||||
ResolveSchemaReferences(document, innerSchema);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ResolveSchemaReferences(OpenApiDocument document, IList<IOpenApiSchema>? schemas)
|
||||
{
|
||||
if (schemas is null || schemas.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < schemas.Count; i++)
|
||||
{
|
||||
IOpenApiSchema allOfSchema = schemas[i];
|
||||
schemas[i] = GetActualSchemaOrReference(document, allOfSchema, out var replaced);
|
||||
if (!replaced)
|
||||
{
|
||||
ResolveSchemaReferences(document, schemas[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[return: NotNullIfNotNull(nameof(schema))]
|
||||
private static IOpenApiSchema? GetActualSchemaOrReference(
|
||||
OpenApiDocument document,
|
||||
IOpenApiSchema? schema,
|
||||
out bool replaced)
|
||||
{
|
||||
if (schema is not OpenApiSchema openApiSchema)
|
||||
{
|
||||
replaced = false;
|
||||
return schema;
|
||||
}
|
||||
|
||||
// Check if this is a placeholder schema (circular reference)
|
||||
if (openApiSchema.Metadata?.TryGetValue(RecursiveRefMetadataKey, out var recursiveRefIdObj) == true
|
||||
&& recursiveRefIdObj is string recursiveRefId)
|
||||
{
|
||||
replaced = true;
|
||||
return new OpenApiSchemaReference(recursiveRefId, document);
|
||||
}
|
||||
|
||||
// Check if this is a componentized schema that should be a $ref
|
||||
// Only resolve if the component actually exists — the framework also sets x-schema-id on
|
||||
// schemas that may not end up as components.
|
||||
if (openApiSchema.Metadata?.TryGetValue(SchemaIdMetadataKey, out var schemaIdObj) == true
|
||||
&& schemaIdObj is string schemaId
|
||||
&& !string.IsNullOrEmpty(schemaId)
|
||||
&& document.Components?.Schemas?.ContainsKey(schemaId) == true)
|
||||
{
|
||||
replaced = true;
|
||||
return new OpenApiSchemaReference(schemaId, document);
|
||||
}
|
||||
|
||||
replaced = false;
|
||||
return schema;
|
||||
}
|
||||
|
||||
private IReadOnlyCollection<ContentTypeSchemaInfo> FilterAllowedDocumentTypes(IReadOnlyCollection<ContentTypeSchemaInfo> documentTypes)
|
||||
{
|
||||
DeliveryApiSettings settings = _deliveryApiSettings.CurrentValue;
|
||||
return documentTypes
|
||||
.Where(c => settings.IsAllowedContentType(c.Alias))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the schemas to use as the <c>allOf</c> bases for each typed content type
|
||||
/// schema in a polymorphic union. Prefers concrete derived types declared on the
|
||||
/// interface via <c>[JsonDerivedType]</c>; when none are advertised, falls back to a
|
||||
/// schema built from the interface's own properties.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The fallback exists for media interfaces, whose concrete classes are internal in
|
||||
/// Umbraco.Infrastructure and therefore cannot be referenced via <c>[JsonDerivedType]</c>
|
||||
/// from Umbraco.Core.
|
||||
/// </remarks>
|
||||
private async Task<List<IOpenApiSchema>> ResolveDerivedTypeSchemas(
|
||||
OpenApiSchema interfaceSchema,
|
||||
OpenApiSchemaTransformerContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
List<IOpenApiSchema> derivedTypeSchemas = [];
|
||||
foreach (JsonDerivedType derivedType in context.JsonTypeInfo.PolymorphismOptions?.DerivedTypes ?? [])
|
||||
{
|
||||
IOpenApiSchema derivedTypeSchema = await CreateSchema(
|
||||
GetJsonTypeInfo(derivedType.DerivedType),
|
||||
context,
|
||||
cancellationToken);
|
||||
derivedTypeSchemas.Add(derivedTypeSchema);
|
||||
}
|
||||
|
||||
if (derivedTypeSchemas.Count == 0)
|
||||
{
|
||||
derivedTypeSchemas.Add(CreateBaseSchemaFromInterface(interfaceSchema, context));
|
||||
}
|
||||
|
||||
return derivedTypeSchemas;
|
||||
}
|
||||
|
||||
private static IOpenApiSchema CreateBaseSchemaFromInterface(
|
||||
OpenApiSchema interfaceSchema,
|
||||
OpenApiSchemaTransformerContext context)
|
||||
{
|
||||
// Append a "Base" marker so this schema stays distinct from the polymorphic union
|
||||
// schema for the same interface (e.g. IApiMediaWithCropsResponseBaseModel vs.
|
||||
// IApiMediaWithCropsResponseModel).
|
||||
var baseSchemaId = $"{context.JsonTypeInfo.Type.Name}Base{ModelSuffix}";
|
||||
OpenApiDocument document = context.GetRequiredDocument();
|
||||
var baseSchema = new OpenApiSchema
|
||||
{
|
||||
Type = interfaceSchema.Type,
|
||||
Properties = interfaceSchema.Properties,
|
||||
Required = interfaceSchema.Required,
|
||||
Metadata = new Dictionary<string, object> { [SchemaIdMetadataKey] = baseSchemaId },
|
||||
};
|
||||
|
||||
document.AddComponent(baseSchemaId, baseSchema);
|
||||
return new OpenApiSchemaReference(baseSchemaId, document);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
using Microsoft.AspNetCore.OpenApi;
|
||||
using Microsoft.OpenApi;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.OpenApi.Transformers;
|
||||
|
||||
internal abstract class DeliveryApiTransformerBase : IOpenApiOperationTransformer
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the link to the relevant documentation section.
|
||||
/// </summary>
|
||||
protected abstract string DocumentationLink { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task TransformAsync(
|
||||
OpenApiOperation operation,
|
||||
OpenApiOperationTransformerContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!ShouldApply(context))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AddExpand(operation);
|
||||
AddFields(operation);
|
||||
|
||||
await ApplyAsync(operation, context, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the transformer should be applied for the given context.
|
||||
/// </summary>
|
||||
/// <param name="context">The operation transformer context.</param>
|
||||
/// <returns>>True if the transformer should be applied; otherwise, false.</returns>
|
||||
protected abstract bool ShouldApply(OpenApiOperationTransformerContext context);
|
||||
|
||||
/// <summary>
|
||||
/// Applies the specific transformations to the OpenAPI operation.
|
||||
/// </summary>
|
||||
/// <param name="operation">The <see cref="OpenApiOperation"/> to modify.</param>
|
||||
/// <param name="context">The <see cref="OpenApiOperationTransformerContext"/> associated with the <see paramref="operation"/>.</param>
|
||||
/// <param name="cancellationToken">The cancellation token to use.</param>
|
||||
/// <returns>The task object representing the asynchronous operation.</returns>
|
||||
protected abstract Task ApplyAsync(
|
||||
OpenApiOperation operation,
|
||||
OpenApiOperationTransformerContext context,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
private void AddExpand(OpenApiOperation operation)
|
||||
{
|
||||
operation.Parameters ??= new List<IOpenApiParameter>();
|
||||
operation.Parameters.Add(
|
||||
new OpenApiParameter
|
||||
{
|
||||
Name = "expand",
|
||||
In = ParameterLocation.Query,
|
||||
Required = false,
|
||||
Description = QueryParameterDescription("Defines the properties that should be expanded in the response"),
|
||||
Schema = new OpenApiSchema { Type = JsonSchemaType.String },
|
||||
Examples = new Dictionary<string, IOpenApiExample>
|
||||
{
|
||||
{ "Expand none", new OpenApiExample { Value = "" } },
|
||||
{ "Expand all properties", new OpenApiExample { Value = "properties[$all]" } },
|
||||
{ "Expand specific property", new OpenApiExample { Value = "properties[alias1]" } },
|
||||
{ "Expand specific properties", new OpenApiExample { Value = "properties[alias1,alias2]" } },
|
||||
{ "Expand nested properties", new OpenApiExample { Value = "properties[alias1[properties[nestedAlias1,nestedAlias2]]]" } },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private void AddFields(OpenApiOperation operation)
|
||||
{
|
||||
operation.Parameters ??= new List<IOpenApiParameter>();
|
||||
operation.Parameters.Add(
|
||||
new OpenApiParameter
|
||||
{
|
||||
Name = "fields",
|
||||
In = ParameterLocation.Query,
|
||||
Required = false,
|
||||
Description =
|
||||
QueryParameterDescription("Explicitly defines which properties should be included in the response (by default all properties are included)"),
|
||||
Schema = new OpenApiSchema { Type = JsonSchemaType.String },
|
||||
Examples = new Dictionary<string, IOpenApiExample>
|
||||
{
|
||||
{ "Include all properties", new OpenApiExample { Value = "properties[$all]" } },
|
||||
{ "Include only specific property", new OpenApiExample { Value = "properties[alias1]" } },
|
||||
{ "Include only specific properties", new OpenApiExample { Value = "properties[alias1,alias2]" } },
|
||||
{ "Include only specific nested properties", new OpenApiExample { Value = "properties[alias1[properties[nestedAlias1,nestedAlias2]]]" } },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
protected void AddQueryParameterDocumentation(OpenApiParameter parameter, Dictionary<string, IOpenApiExample> examples, string description)
|
||||
{
|
||||
parameter.Description = QueryParameterDescription(description);
|
||||
parameter.Examples = examples;
|
||||
}
|
||||
|
||||
protected string PaginationDescription(bool skip, string itemType)
|
||||
=> $"Specifies the number of found {itemType} items to {(skip ? "skip" : "take")}. Use this to control pagination of the response.";
|
||||
|
||||
private string QueryParameterDescription(string description)
|
||||
=> $"{description}. Refer to [the documentation]({DocumentationLink}#query-parameters) for more details on this.";
|
||||
}
|
||||
+33
-19
@@ -1,27 +1,41 @@
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.AspNetCore.OpenApi;
|
||||
using Microsoft.OpenApi;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using Umbraco.Cms.Api.Delivery.Configuration;
|
||||
using Umbraco.Cms.Api.Delivery.Controllers.Media;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Filters;
|
||||
namespace Umbraco.Cms.Api.Delivery.OpenApi.Transformers;
|
||||
|
||||
internal sealed class SwaggerMediaDocumentationFilter : SwaggerDocumentationFilterBase<MediaApiControllerBase>
|
||||
/// <summary>
|
||||
/// Transforms OpenAPI operations for the Media API, adding relevant parameters and documentation.
|
||||
/// </summary>
|
||||
internal sealed class MediaApiTransformer : DeliveryApiTransformerBase
|
||||
{
|
||||
protected override string DocumentationLink => DeliveryApiConfiguration.ApiDocumentationMediaArticleLink;
|
||||
|
||||
protected override void ApplyOperation(OpenApiOperation operation, OperationFilterContext context)
|
||||
/// <inheritdoc/>
|
||||
protected override bool ShouldApply(OpenApiOperationTransformerContext context) =>
|
||||
context.Description.ActionDescriptor is ControllerActionDescriptor description
|
||||
&& description.ControllerTypeInfo.Implements<MediaApiControllerBase>();
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Task ApplyAsync(
|
||||
OpenApiOperation operation,
|
||||
OpenApiOperationTransformerContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
operation.Parameters ??= new List<IOpenApiParameter>();
|
||||
foreach (OpenApiParameter parameter in operation.Parameters?.OfType<OpenApiParameter>() ?? [])
|
||||
{
|
||||
ApplyParameter(parameter);
|
||||
}
|
||||
|
||||
AddExpand(operation, context);
|
||||
|
||||
AddFields(operation, context);
|
||||
|
||||
AddApiKey(operation);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
protected override void ApplyParameter(OpenApiParameter parameter, ParameterFilterContext context)
|
||||
private void ApplyParameter(OpenApiParameter parameter)
|
||||
{
|
||||
switch (parameter.Name)
|
||||
{
|
||||
@@ -56,18 +70,18 @@ internal sealed class SwaggerMediaDocumentationFilter : SwaggerDocumentationFilt
|
||||
private Dictionary<string, IOpenApiExample> FilterQueryParameterExamples() =>
|
||||
new()
|
||||
{
|
||||
{ "Default filter", new OpenApiExample { Value = string.Empty } },
|
||||
{ "Filter by media type", new OpenApiExample { Value = new JsonArray { "mediaType:alias1" } } },
|
||||
{ "Filter by name", new OpenApiExample { Value = new JsonArray { "name:nodeName" } } },
|
||||
{ "Default filter", new OpenApiExample { Value = new JsonArray(string.Empty) } },
|
||||
{ "Filter by media type", new OpenApiExample { Value = new JsonArray("mediaType:alias1") } },
|
||||
{ "Filter by name", new OpenApiExample { Value = new JsonArray("name:nodeName") } },
|
||||
};
|
||||
|
||||
private Dictionary<string, IOpenApiExample> SortQueryParameterExamples() =>
|
||||
new()
|
||||
{
|
||||
{ "Default sort", new OpenApiExample { Value = string.Empty } },
|
||||
{ "Sort by create date", new OpenApiExample { Value = new JsonArray { "createDate:asc", "createDate:desc" } } },
|
||||
{ "Sort by name", new OpenApiExample { Value = new JsonArray { "name:asc", "name:desc" } } },
|
||||
{ "Sort by sort order", new OpenApiExample { Value = new JsonArray { "sortOrder:asc", "sortOrder:desc" } } },
|
||||
{ "Sort by update date", new OpenApiExample { Value = new JsonArray { "updateDate:asc", "updateDate:desc" } } },
|
||||
{ "Default sort", new OpenApiExample { Value = new JsonArray(string.Empty) } },
|
||||
{ "Sort by create date", new OpenApiExample { Value = new JsonArray("createDate:asc", "createDate:desc") } },
|
||||
{ "Sort by name", new OpenApiExample { Value = new JsonArray("name:asc", "name:desc") } },
|
||||
{ "Sort by sort order", new OpenApiExample { Value = new JsonArray("sortOrder:asc", "sortOrder:desc") } },
|
||||
{ "Sort by update date", new OpenApiExample { Value = new JsonArray("updateDate:asc", "updateDate:desc") } },
|
||||
};
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
using Microsoft.AspNetCore.OpenApi;
|
||||
using Microsoft.OpenApi;
|
||||
using Umbraco.Cms.Api.Common.Security;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.OpenApi.Transformers;
|
||||
|
||||
/// <summary>
|
||||
/// Transformer that adds member authentication security requirements to OpenAPI documents.
|
||||
/// </summary>
|
||||
internal class MemberAuthenticationSecurityRequirementsTransformer : IOpenApiOperationTransformer, IOpenApiDocumentTransformer
|
||||
{
|
||||
private const string AuthSchemeName = "UmbracoMember";
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task TransformAsync(
|
||||
OpenApiDocument document,
|
||||
OpenApiDocumentTransformerContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var securityScheme = new OpenApiSecurityScheme
|
||||
{
|
||||
In = ParameterLocation.Header,
|
||||
Name = AuthSchemeName,
|
||||
Type = SecuritySchemeType.OAuth2,
|
||||
Description = "Umbraco Member Authentication",
|
||||
Flows = new OpenApiOAuthFlows
|
||||
{
|
||||
AuthorizationCode = new OpenApiOAuthFlow
|
||||
{
|
||||
AuthorizationUrl = new Uri(Paths.MemberApi.AuthorizationEndpoint, UriKind.Relative),
|
||||
TokenUrl = new Uri(Paths.MemberApi.TokenEndpoint, UriKind.Relative),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
document.AddComponent(AuthSchemeName, securityScheme);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task TransformAsync(
|
||||
OpenApiOperation operation,
|
||||
OpenApiOperationTransformerContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var schemaRef = new OpenApiSecuritySchemeReference(AuthSchemeName, context.Document);
|
||||
operation.Security ??= new List<OpenApiSecurityRequirement>();
|
||||
operation.Security.Add(new OpenApiSecurityRequirement { [schemaRef] = [] });
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ internal sealed class DeliveryApiItemsEndpointsMatcherPolicy : MatcherPolicy, IE
|
||||
public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates)
|
||||
{
|
||||
var hasIdQueryParameter = httpContext.Request.Query.ContainsKey("id");
|
||||
ApiVersion? requestedApiVersion = httpContext.GetRequestedApiVersion();
|
||||
ApiVersion? requestedApiVersion = httpContext.RequestedApiVersion;
|
||||
for (var i = 0; i < candidates.Count; i++)
|
||||
{
|
||||
CandidateState candidate = candidates[i];
|
||||
|
||||
@@ -18,6 +18,7 @@ internal sealed class RequestRedirectService : RoutingServiceBase, IRequestRedir
|
||||
private readonly IRedirectUrlService _redirectUrlService;
|
||||
private readonly IApiPublishedContentCache _apiPublishedContentCache;
|
||||
private readonly IApiContentRouteBuilder _apiContentRouteBuilder;
|
||||
private readonly IDocumentUrlService _documentUrlService;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
|
||||
public RequestRedirectService(
|
||||
@@ -28,13 +29,15 @@ internal sealed class RequestRedirectService : RoutingServiceBase, IRequestRedir
|
||||
IRedirectUrlService redirectUrlService,
|
||||
IApiPublishedContentCache apiPublishedContentCache,
|
||||
IApiContentRouteBuilder apiContentRouteBuilder,
|
||||
IOptions<GlobalSettings> globalSettings)
|
||||
IOptions<GlobalSettings> globalSettings,
|
||||
IDocumentUrlService documentUrlService)
|
||||
: base(domainCache, httpContextAccessor, requestStartItemProviderAccessor)
|
||||
{
|
||||
_requestCultureService = requestCultureService;
|
||||
_redirectUrlService = redirectUrlService;
|
||||
_apiPublishedContentCache = apiPublishedContentCache;
|
||||
_apiContentRouteBuilder = apiContentRouteBuilder;
|
||||
_documentUrlService = documentUrlService;
|
||||
_globalSettings = globalSettings.Value;
|
||||
}
|
||||
|
||||
@@ -43,16 +46,19 @@ internal sealed class RequestRedirectService : RoutingServiceBase, IRequestRedir
|
||||
requestedPath = requestedPath.EnsureStartsWith("/");
|
||||
|
||||
IPublishedContent? startItem = GetStartItem();
|
||||
var culture = _requestCultureService.GetRequestedCulture();
|
||||
|
||||
// must append the root content url segment if it is not hidden by config, because
|
||||
// the URL tracking is based on the actual URL, including the root content url segment
|
||||
if (_globalSettings.HideTopLevelNodeFromPath == false && startItem?.UrlSegment != null)
|
||||
if (_globalSettings.HideTopLevelNodeFromPath == false && startItem is not null)
|
||||
{
|
||||
requestedPath = $"{startItem.UrlSegment.EnsureStartsWith("/")}{requestedPath}";
|
||||
var startItemUrlSegment = _documentUrlService.GetUrlSegment(startItem.Key, culture ?? string.Empty, isDraft: false);
|
||||
if (startItemUrlSegment is not null)
|
||||
{
|
||||
requestedPath = $"{startItemUrlSegment.EnsureStartsWith("/")}{requestedPath}";
|
||||
}
|
||||
}
|
||||
|
||||
var culture = _requestCultureService.GetRequestedCulture();
|
||||
|
||||
// important: redirect URLs are always tracked without trailing slashes
|
||||
requestedPath = requestedPath.TrimEnd("/");
|
||||
IRedirectUrl? redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl(requestedPath, culture);
|
||||
|
||||
@@ -3,7 +3,7 @@ using Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Services;
|
||||
|
||||
internal sealed class RequestSegmentService : RequestHeaderHandler, IRequestSegmentService, IRequestSegmmentService
|
||||
internal sealed class RequestSegmentService : RequestHeaderHandler, IRequestSegmentService
|
||||
{
|
||||
public RequestSegmentService(IHttpContextAccessor httpContextAccessor)
|
||||
: base(httpContextAccessor)
|
||||
|
||||
@@ -3,6 +3,7 @@ using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.Navigation;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
@@ -14,6 +15,7 @@ internal sealed class RequestStartItemProvider : RequestHeaderHandler, IRequestS
|
||||
private readonly IRequestPreviewService _requestPreviewService;
|
||||
private readonly IDocumentNavigationQueryService _documentNavigationQueryService;
|
||||
private readonly IPublishedContentCache _publishedContentCache;
|
||||
private readonly IDocumentUrlService _documentUrlService;
|
||||
|
||||
// this provider lifetime is Scope, so we can cache this as a field
|
||||
private IPublishedContent? _requestedStartContent;
|
||||
@@ -23,14 +25,15 @@ internal sealed class RequestStartItemProvider : RequestHeaderHandler, IRequestS
|
||||
IVariationContextAccessor variationContextAccessor,
|
||||
IRequestPreviewService requestPreviewService,
|
||||
IDocumentNavigationQueryService documentNavigationQueryService,
|
||||
IPublishedContentCache publishedContentCache)
|
||||
IPublishedContentCache publishedContentCache,
|
||||
IDocumentUrlService documentUrlService)
|
||||
: base(httpContextAccessor)
|
||||
{
|
||||
|
||||
_variationContextAccessor = variationContextAccessor;
|
||||
_requestPreviewService = requestPreviewService;
|
||||
_documentNavigationQueryService = documentNavigationQueryService;
|
||||
_publishedContentCache = publishedContentCache;
|
||||
_documentUrlService = documentUrlService;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -47,14 +50,16 @@ internal sealed class RequestStartItemProvider : RequestHeaderHandler, IRequestS
|
||||
return null;
|
||||
}
|
||||
|
||||
var isPreview = _requestPreviewService.IsPreview();
|
||||
_documentNavigationQueryService.TryGetRootKeys(out IEnumerable<Guid> rootKeys);
|
||||
IEnumerable<IPublishedContent> rootContent = rootKeys
|
||||
.Select(rootKey => _publishedContentCache.GetById(_requestPreviewService.IsPreview(), rootKey))
|
||||
.Select(rootKey => _publishedContentCache.GetById(isPreview, rootKey))
|
||||
.WhereNotNull();
|
||||
|
||||
var culture = _variationContextAccessor.VariationContext?.Culture ?? string.Empty;
|
||||
_requestedStartContent = Guid.TryParse(headerValue, out Guid key)
|
||||
? rootContent.FirstOrDefault(c => c.Key == key)
|
||||
: rootContent.FirstOrDefault(c => c.UrlSegment(_variationContextAccessor).InvariantEquals(headerValue));
|
||||
: rootContent.FirstOrDefault(c => _documentUrlService.GetUrlSegment(c.Key, culture, isPreview).InvariantEquals(headerValue));
|
||||
|
||||
return _requestedStartContent;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ RESTful API for Umbraco backoffice operations. Manages content, media, users, an
|
||||
|
||||
### Key Technologies
|
||||
- **Web Framework**: ASP.NET Core MVC with `Asp.Versioning.Mvc` (v1.0 currently)
|
||||
- **OpenAPI**: Swashbuckle.AspNetCore with custom schema/operation filters
|
||||
- **OpenAPI**: Microsoft.AspNetCore.OpenApi with custom transformers, Swagger UI via Swashbuckle
|
||||
- **Authentication**: OpenIddict via `Umbraco.Cms.Api.Common` (reference tokens, not JWT)
|
||||
- **Authorization**: Policy-based with `IAuthorizationService`
|
||||
- **Validation**: FluentValidation via base controllers
|
||||
@@ -57,7 +57,7 @@ src/Umbraco.Cms.Api.Management/
|
||||
├── Services/ # Business logic (thin layer over Core.Services)
|
||||
├── Mapping/ # ViewModel → domain model mappers
|
||||
├── Security/ # Auth providers, sign-in manager, external logins
|
||||
├── OpenApi/ # Swashbuckle filters (schema, operation, security)
|
||||
├── OpenApi/ # OpenAPI transformers (schema, operation, security)
|
||||
├── Routing/ # Route configuration, SignalR hubs
|
||||
├── DependencyInjection/ # Service registration (55+ files)
|
||||
├── Middleware/ # Preview, server events
|
||||
@@ -71,7 +71,8 @@ src/Umbraco.Cms.Api.Management/
|
||||
- **Umbraco.Cms.Api.Common** - Shared API infrastructure (base controllers, OpenAPI config)
|
||||
- **Umbraco.Infrastructure** - Service implementations, data access
|
||||
- **Umbraco.PublishedCache.HybridCache** - Published content queries
|
||||
- **Swashbuckle.AspNetCore** - OpenAPI generation
|
||||
- **Microsoft.AspNetCore.OpenApi** - OpenAPI document generation
|
||||
- **Swashbuckle.AspNetCore.SwaggerUI** - Swagger UI
|
||||
|
||||
### Design Patterns
|
||||
1. **Controller-per-Operation** - Each endpoint is a separate controller class
|
||||
@@ -122,8 +123,8 @@ dotnet build src/Umbraco.Cms.Api.Management /p:TreatWarningsAsErrors=true
|
||||
### OpenAPI Documentation
|
||||
The project embeds a pre-generated `OpenApi.json` (1.3MB). To regenerate:
|
||||
```bash
|
||||
# Run Umbraco.Web.UI, access /umbraco/swagger
|
||||
# Export JSON from Swagger UI
|
||||
# Run Umbraco.Web.UI, access /umbraco/openapi/
|
||||
# Download JSON from /umbraco/openapi/management.json
|
||||
```
|
||||
|
||||
### Package Management
|
||||
@@ -201,7 +202,7 @@ dotnet test --filter "FullyQualifiedName~Management.Controllers.Document"
|
||||
1. **Controller logic** - Request validation, authorization checks, status code mapping
|
||||
2. **Factories** - ViewModel ↔ Domain model conversion accuracy
|
||||
3. **Authorization** - Policy enforcement for each operation
|
||||
4. **OpenAPI schema** - Ensure Swagger generation doesn't break
|
||||
4. **OpenAPI schema** - Ensure OpenAPI document generation doesn't break
|
||||
|
||||
### InternalsVisibleTo
|
||||
Tests have access to internal types (see .csproj:44-52):
|
||||
@@ -465,7 +466,7 @@ TODO: [NL] This must return path segments for a query to work
|
||||
1. All tests pass
|
||||
2. Code formatted (`dotnet format`)
|
||||
3. No new warnings (check suppressed warnings list in .csproj:23)
|
||||
4. OpenAPI schema valid (run Swagger UI)
|
||||
4. OpenAPI schema valid (check at `/umbraco/openapi/`)
|
||||
5. Authorization tested (unit + integration tests)
|
||||
|
||||
### Common Pitfalls
|
||||
@@ -612,7 +613,7 @@ Examples:
|
||||
- `PUT /umbraco/management/api/v1/document/{id}` - Update document
|
||||
- `DELETE /umbraco/management/api/v1/document/{id}` - Delete document
|
||||
|
||||
Full spec: See OpenApi.json or Swagger UI at `/umbraco/swagger`
|
||||
Full spec: See OpenApi.json or Swagger UI at `/umbraco/openapi/`
|
||||
|
||||
### Getting Help
|
||||
- **Root Documentation**: `/CLAUDE.md` (repository overview)
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- https://learn.microsoft.com/dotnet/fundamentals/package-validation/diagnostic-ids -->
|
||||
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Umbraco.Cms.Api.Management.Controllers.Document.GetPublicAccessDocumentController.GetPublicAccess(System.Threading.CancellationToken,System.Guid)</Target>
|
||||
<Left>lib/net10.0/Umbraco.Cms.Api.Management.dll</Left>
|
||||
<Right>lib/net10.0/Umbraco.Cms.Api.Management.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Umbraco.Cms.Api.Management.Controllers.UrlSegment.ResizeImagingController.Urls(System.Collections.Generic.HashSet{System.Guid},System.Int32,System.Int32,System.Nullable{Umbraco.Cms.Core.Models.ImageCropMode})</Target>
|
||||
<Left>lib/net10.0/Umbraco.Cms.Api.Management.dll</Left>
|
||||
<Right>lib/net10.0/Umbraco.Cms.Api.Management.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
</Suppressions>
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
using Microsoft.AspNetCore.OpenApi;
|
||||
using Umbraco.Cms.Api.Common.Configuration;
|
||||
using Umbraco.Cms.Api.Common.OpenApi;
|
||||
using Umbraco.Cms.Api.Management.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Management.OpenApi;
|
||||
using Umbraco.Cms.Api.Management.OpenApi.Transformers;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Configures the OpenAPI options for the Umbraco Management API.
|
||||
/// </summary>
|
||||
internal class ConfigureUmbracoManagementApiOpenApiOptions : ConfigureUmbracoOpenApiOptionsBase
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override string ApiName => ManagementApiConfiguration.ApiName;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string ApiTitle => ManagementApiConfiguration.ApiTitle;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string ApiVersion => "Latest";
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string ApiDescription =>
|
||||
"This shows all APIs available in this version of Umbraco - including all the legacy apis that are available for backward compatibility";
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void ConfigureOpenApi(OpenApiOptions options)
|
||||
{
|
||||
base.ConfigureOpenApi(options);
|
||||
|
||||
// Sets Security requirement on backoffice apis
|
||||
options.AddBackofficeSecurityRequirements();
|
||||
|
||||
options.AddSchemaTransformer<RequireNonNullablePropertiesSchemaTransformer>();
|
||||
options.AddSchemaTransformer<FixFileReturnTypesTransformer>();
|
||||
options.AddOperationTransformer<MimeTypesTransformer>();
|
||||
options.AddOperationTransformer<ResponseHeaderTransformer>();
|
||||
options.AddOperationTransformer<NotificationHeaderTransformer>();
|
||||
}
|
||||
}
|
||||
-79
@@ -1,79 +0,0 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.OpenApi;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using Umbraco.Cms.Api.Common.Security;
|
||||
using Umbraco.Cms.Api.Common.Serialization;
|
||||
using Umbraco.Cms.Api.Management.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Management.OpenApi;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Provides configuration for Swagger generation options specific to the Umbraco Management API.
|
||||
/// This class is used to customize the Swagger documentation for the API endpoints.
|
||||
/// </summary>
|
||||
public class ConfigureUmbracoManagementApiSwaggerGenOptions : IConfigureOptions<SwaggerGenOptions>
|
||||
{
|
||||
private readonly IUmbracoJsonTypeInfoResolver _umbracoJsonTypeInfoResolver;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConfigureUmbracoManagementApiSwaggerGenOptions"/> class.
|
||||
/// </summary>
|
||||
/// <param name="umbracoJsonTypeInfoResolver">An instance of <see cref="IUmbracoJsonTypeInfoResolver"/> used to resolve JSON type information for Umbraco.</param>
|
||||
public ConfigureUmbracoManagementApiSwaggerGenOptions(IUmbracoJsonTypeInfoResolver umbracoJsonTypeInfoResolver)
|
||||
{
|
||||
_umbracoJsonTypeInfoResolver = umbracoJsonTypeInfoResolver;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures the <see cref="SwaggerGenOptions"/> for the Umbraco Management API.
|
||||
/// Sets up the Swagger documentation, including API metadata, security definitions for OAuth2 authentication,
|
||||
/// operation filters for response headers and security requirements, and schema filters for non-nullable properties.
|
||||
/// Also configures polymorphism handling and discriminator properties for OpenAPI schemas.
|
||||
/// </summary>
|
||||
/// <param name="swaggerGenOptions">The <see cref="SwaggerGenOptions"/> instance to configure for the Management API.</param>
|
||||
public void Configure(SwaggerGenOptions swaggerGenOptions)
|
||||
{
|
||||
swaggerGenOptions.SwaggerDoc(
|
||||
ManagementApiConfiguration.ApiName,
|
||||
new OpenApiInfo
|
||||
{
|
||||
Title = ManagementApiConfiguration.ApiTitle,
|
||||
Version = "Latest",
|
||||
Description = "This shows all APIs available in this version of Umbraco - including all the legacy apis that are available for backward compatibility",
|
||||
});
|
||||
|
||||
swaggerGenOptions.OperationFilter<ResponseHeaderOperationFilter>();
|
||||
swaggerGenOptions.UseOneOfForPolymorphism();
|
||||
|
||||
// Ensure all types that implements the IOpenApiDiscriminator have a $type property in the OpenApi schema with the default value (The class name) that is expected by the server
|
||||
swaggerGenOptions.SelectDiscriminatorNameUsing(type => _umbracoJsonTypeInfoResolver.GetTypeDiscriminatorValue(type) is not null ? "$type" : null);
|
||||
swaggerGenOptions.SelectDiscriminatorValueUsing(_umbracoJsonTypeInfoResolver.GetTypeDiscriminatorValue);
|
||||
|
||||
|
||||
swaggerGenOptions.AddSecurityDefinition(
|
||||
ManagementApiConfiguration.ApiSecurityName,
|
||||
new OpenApiSecurityScheme
|
||||
{
|
||||
In = ParameterLocation.Header,
|
||||
Name = "Umbraco",
|
||||
Type = SecuritySchemeType.OAuth2,
|
||||
Description = "Umbraco Authentication",
|
||||
Flows = new OpenApiOAuthFlows
|
||||
{
|
||||
AuthorizationCode = new OpenApiOAuthFlow
|
||||
{
|
||||
AuthorizationUrl =
|
||||
new Uri(Paths.BackOfficeApi.AuthorizationEndpoint, UriKind.Relative),
|
||||
TokenUrl = new Uri(Paths.BackOfficeApi.TokenEndpoint, UriKind.Relative),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Sets Security requirement on backoffice apis
|
||||
swaggerGenOptions.OperationFilter<BackOfficeSecurityRequirementsOperationFilter>();
|
||||
swaggerGenOptions.OperationFilter<NotificationHeaderFilter>();
|
||||
swaggerGenOptions.SchemaFilter<RequireNonNullablePropertiesSchemaFilter>();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Document;
|
||||
using Umbraco.Cms.Core.Models.ContentEditing;
|
||||
using Umbraco.Cms.Core.Models.ContentEditing.Validation;
|
||||
using Umbraco.Cms.Core.Models.ContentPublishing;
|
||||
using Umbraco.Cms.Core.PropertyEditors.Validation;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Extensions;
|
||||
@@ -13,6 +15,8 @@ namespace Umbraco.Cms.Api.Management.Controllers.Content;
|
||||
/// </summary>
|
||||
public abstract class ContentControllerBase : ManagementApiControllerBase
|
||||
{
|
||||
protected abstract string EntityName { get; }
|
||||
|
||||
protected IActionResult ContentEditingOperationStatusResult(ContentEditingOperationStatus status)
|
||||
=> OperationStatusResult(status, problemDetailsBuilder => status switch
|
||||
{
|
||||
@@ -90,12 +94,12 @@ public abstract class ContentControllerBase : ManagementApiControllerBase
|
||||
.WithDetail("The supplied name is already in use for the same content type.")
|
||||
.Build()),
|
||||
ContentEditingOperationStatus.CannotDeleteWhenReferenced => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Cannot delete a referenced content item")
|
||||
.WithDetail("Cannot delete a referenced content item, while the setting ContentSettings.DisableDeleteWhenReferenced is enabled.")
|
||||
.WithTitle($"Cannot delete a referenced {EntityName}")
|
||||
.WithDetail($"Cannot delete a referenced {EntityName}, while the setting ContentSettings.DisableDeleteWhenReferenced is enabled.")
|
||||
.Build()),
|
||||
ContentEditingOperationStatus.CannotMoveToRecycleBinWhenReferenced => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Cannot move a referenced content item to the recycle bin")
|
||||
.WithDetail("Cannot move a referenced content item to the recycle bin, while the setting ContentSettings.DisableDeleteWhenReferenced is enabled.")
|
||||
.WithTitle($"Cannot move a referenced {EntityName} to the recycle bin")
|
||||
.WithDetail($"Cannot move a referenced {EntityName} to the recycle bin, while the setting ContentSettings.DisableUnpublishWhenReferenced is enabled.")
|
||||
.Build()),
|
||||
ContentEditingOperationStatus.Unknown => StatusCode(
|
||||
StatusCodes.Status500InternalServerError,
|
||||
@@ -118,6 +122,122 @@ public abstract class ContentControllerBase : ManagementApiControllerBase
|
||||
.Build()),
|
||||
});
|
||||
|
||||
protected IActionResult ContentPublishingOperationStatusResult(
|
||||
ContentPublishingOperationStatus status,
|
||||
IEnumerable<string>? invalidPropertyAliases = null,
|
||||
IEnumerable<ContentPublishingBranchItemResult>? failedBranchItems = null)
|
||||
=> OperationStatusResult(
|
||||
status,
|
||||
problemDetailsBuilder => status switch
|
||||
{
|
||||
ContentPublishingOperationStatus.ContentNotFound => NotFound(problemDetailsBuilder
|
||||
.WithTitle($"The requested {EntityName} could not be found")
|
||||
.Build()),
|
||||
ContentPublishingOperationStatus.CancelledByEvent => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Publish cancelled by event")
|
||||
.WithDetail("The publish operation was cancelled by an event.")
|
||||
.Build()),
|
||||
ContentPublishingOperationStatus.ContentInvalid => BadRequest(problemDetailsBuilder
|
||||
.WithTitle($"Invalid {EntityName}")
|
||||
.WithDetail($"The specified {EntityName} had an invalid configuration.")
|
||||
.WithExtension("invalidProperties", invalidPropertyAliases ?? Enumerable.Empty<string>())
|
||||
.Build()),
|
||||
ContentPublishingOperationStatus.NothingToPublish => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Nothing to publish")
|
||||
.WithDetail("None of the specified cultures needed publishing.")
|
||||
.Build()),
|
||||
ContentPublishingOperationStatus.MandatoryCultureMissing => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Mandatory culture missing")
|
||||
.WithDetail("Must include all mandatory cultures when publishing.")
|
||||
.Build()),
|
||||
ContentPublishingOperationStatus.HasExpired => BadRequest(problemDetailsBuilder
|
||||
.WithTitle($"{EntityName.ToFirstUpperInvariant()} expired")
|
||||
.WithDetail($"Could not publish the {EntityName} because it was expired.")
|
||||
.Build()),
|
||||
ContentPublishingOperationStatus.CultureHasExpired => BadRequest(problemDetailsBuilder
|
||||
.WithTitle($"{EntityName.ToFirstUpperInvariant()} culture expired")
|
||||
.WithDetail($"Could not publish the {EntityName} because some of the specified cultures were expired.")
|
||||
.Build()),
|
||||
ContentPublishingOperationStatus.AwaitingRelease => BadRequest(problemDetailsBuilder
|
||||
.WithTitle($"{EntityName.ToFirstUpperInvariant()} awaiting release")
|
||||
.WithDetail($"Could not publish the {EntityName} because it was awaiting release.")
|
||||
.Build()),
|
||||
ContentPublishingOperationStatus.CultureAwaitingRelease => BadRequest(problemDetailsBuilder
|
||||
.WithTitle($"{EntityName.ToFirstUpperInvariant()} culture awaiting release")
|
||||
.WithDetail(
|
||||
$"Could not publish the {EntityName} because some of the specified cultures were awaiting release.")
|
||||
.Build()),
|
||||
ContentPublishingOperationStatus.InTrash => BadRequest(problemDetailsBuilder
|
||||
.WithTitle($"{EntityName.ToFirstUpperInvariant()} in the recycle bin")
|
||||
.WithDetail($"Could not publish the {EntityName} because it was in the recycle bin.")
|
||||
.Build()),
|
||||
ContentPublishingOperationStatus.PathNotPublished => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Parent not published")
|
||||
.WithDetail($"Could not publish the {EntityName} because its parent was not published.")
|
||||
.Build()),
|
||||
ContentPublishingOperationStatus.InvalidCulture => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Invalid cultures specified")
|
||||
.WithDetail("A specified culture is not valid for the operation.")
|
||||
.Build()),
|
||||
ContentPublishingOperationStatus.CultureMissing => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Culture missing")
|
||||
.WithDetail("A culture needs to be specified to execute the operation.")
|
||||
.Build()),
|
||||
ContentPublishingOperationStatus.CannotPublishInvariantWhenVariant => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Cannot publish invariant when variant")
|
||||
.WithDetail($"Cannot publish invariant culture when the {EntityName} varies by culture.")
|
||||
.Build()),
|
||||
ContentPublishingOperationStatus.CannotPublishVariantWhenNotVariant => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Cannot publish variant when not variant.")
|
||||
.WithDetail($"Cannot publish a given culture when the {EntityName} is invariant.")
|
||||
.Build()),
|
||||
ContentPublishingOperationStatus.ConcurrencyViolation => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Concurrency violation detected")
|
||||
.WithDetail("An attempt was made to publish a version older than the latest version.")
|
||||
.Build()),
|
||||
ContentPublishingOperationStatus.UnsavedChanges => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Unsaved changes")
|
||||
.WithDetail(
|
||||
$"Could not publish the {EntityName} because it had unsaved changes. Make sure to save all changes before attempting a publish.")
|
||||
.Build()),
|
||||
ContentPublishingOperationStatus.UnpublishTimeNeedsToBeAfterPublishTime => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Unpublish time needs to be after the publish time")
|
||||
.WithDetail(
|
||||
"Cannot handle an unpublish time that is not after the specified publish time.")
|
||||
.Build()),
|
||||
ContentPublishingOperationStatus.PublishTimeNeedsToBeInFuture => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Publish time needs to be higher than the current time")
|
||||
.WithDetail(
|
||||
"Cannot handle a publish time that is not after the current server time.")
|
||||
.Build()),
|
||||
ContentPublishingOperationStatus.UpublishTimeNeedsToBeInFuture => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Unpublish time needs to be higher than the current time")
|
||||
.WithDetail(
|
||||
"Cannot handle an unpublish time that is not after the current server time.")
|
||||
.Build()),
|
||||
ContentPublishingOperationStatus.CannotUnpublishWhenReferenced => BadRequest(problemDetailsBuilder
|
||||
.WithTitle($"Cannot unpublish {EntityName} when it's referenced somewhere else.")
|
||||
.WithDetail(
|
||||
$"Cannot unpublish a referenced {EntityName}, while the setting ContentSettings.DisableUnpublishWhenReferenced is enabled.")
|
||||
.Build()),
|
||||
ContentPublishingOperationStatus.FailedBranch => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Failed branch operation")
|
||||
.WithDetail("One or more items in the branch could not complete the operation.")
|
||||
.WithExtension("failedBranchItems", failedBranchItems?.Select(item => new DocumentPublishBranchItemResult { Id = item.Key, OperationStatus = item.OperationStatus }) ?? [])
|
||||
.Build()),
|
||||
ContentPublishingOperationStatus.Failed => BadRequest(
|
||||
problemDetailsBuilder
|
||||
.WithTitle("Publish or unpublish failed")
|
||||
.WithDetail(
|
||||
"An unspecified error occurred while (un)publishing. Please check the logs for additional information.")
|
||||
.Build()),
|
||||
ContentPublishingOperationStatus.TaskResultNotFound => NotFound(problemDetailsBuilder
|
||||
.WithTitle("The result of the submitted task could not be found")
|
||||
.Build()),
|
||||
|
||||
_ => StatusCode(StatusCodes.Status500InternalServerError, "Unknown content operation status."),
|
||||
});
|
||||
|
||||
protected IActionResult ContentEditingOperationStatusResult<TContentModelBase, TValueModel, TVariantModel>(
|
||||
ContentEditingOperationStatus status,
|
||||
TContentModelBase requestModel,
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace Umbraco.Cms.Api.Management.Controllers.DataType;
|
||||
/// </summary>
|
||||
[VersionedApiBackOfficeRoute(Constants.UdiEntityType.DataType)]
|
||||
[ApiExplorerSettings(GroupName = "Data Type")]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentsOrMediaOrMembersOrContentTypes)]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentsOrElementsOrMediaOrMembersOrContentTypes)]
|
||||
public abstract class DataTypeControllerBase : ManagementApiControllerBase
|
||||
{
|
||||
protected IActionResult DataTypeOperationStatusResult(DataTypeOperationStatus status) =>
|
||||
|
||||
+3
-6
@@ -53,14 +53,11 @@ public class SearchDataTypeItemController : DatatypeItemControllerBase
|
||||
return Ok(new PagedModel<DataTypeItemResponseModel> { Total = searchResult.Total });
|
||||
}
|
||||
|
||||
Guid[] keys = searchResult.Items.Select(x => x.Key).ToArray();
|
||||
IEnumerable<IDataType> dataTypes = await _dataTypeService.GetAllAsync(keys);
|
||||
IEnumerable<IDataType> orderedDataTypes = OrderByRequestedIds(dataTypes, keys);
|
||||
|
||||
IEnumerable<IDataType> dataTypes = await _dataTypeService.GetAllAsync(searchResult.Items.Select(item => item.Key).ToArray());
|
||||
var result = new PagedModel<DataTypeItemResponseModel>
|
||||
{
|
||||
Items = _mapper.MapEnumerable<IDataType, DataTypeItemResponseModel>(orderedDataTypes),
|
||||
Total = searchResult.Total,
|
||||
Items = _mapper.MapEnumerable<IDataType, DataTypeItemResponseModel>(dataTypes),
|
||||
Total = searchResult.Total
|
||||
};
|
||||
|
||||
return Ok(result);
|
||||
|
||||
-18
@@ -14,24 +14,6 @@ namespace Umbraco.Cms.Api.Management.Controllers.DataType.Tree;
|
||||
[ApiVersion("1.0")]
|
||||
public class AncestorsDataTypeTreeController : DataTypeTreeControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AncestorsDataTypeTreeController"/> class, which provides API endpoints for retrieving ancestor data types in the tree structure.
|
||||
/// </summary>
|
||||
/// <param name="entityService">Service used for entity operations within the API.</param>
|
||||
/// <param name="dataTypeService">Service used for data type management and retrieval.</param>
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")]
|
||||
public AncestorsDataTypeTreeController(IEntityService entityService, IDataTypeService dataTypeService)
|
||||
: base(entityService, dataTypeService)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AncestorsDataTypeTreeController"/> class, which manages operations related to ancestor data type trees in the Umbraco CMS.
|
||||
/// </summary>
|
||||
/// <param name="entityService">Service used for entity-related operations.</param>
|
||||
/// <param name="flagProviders">A collection of providers that supply flags for tree nodes.</param>
|
||||
/// <param name="dataTypeService">Service used for data type management operations.</param>
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public AncestorsDataTypeTreeController(IEntityService entityService, FlagProviderCollection flagProviders, IDataTypeService dataTypeService)
|
||||
: base(entityService, flagProviders, dataTypeService)
|
||||
{
|
||||
|
||||
-18
@@ -15,24 +15,6 @@ namespace Umbraco.Cms.Api.Management.Controllers.DataType.Tree;
|
||||
[ApiVersion("1.0")]
|
||||
public class ChildrenDataTypeTreeController : DataTypeTreeControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ChildrenDataTypeTreeController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="entityService">Service used for managing and retrieving entities within the system.</param>
|
||||
/// <param name="dataTypeService">Service used for managing and retrieving data types.</param>
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")]
|
||||
public ChildrenDataTypeTreeController(IEntityService entityService, IDataTypeService dataTypeService)
|
||||
: base(entityService, dataTypeService)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ChildrenDataTypeTreeController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="entityService">Service used for managing and retrieving entities within the system.</param>
|
||||
/// <param name="flagProviders">A collection of providers that supply additional flags or metadata for entities.</param>
|
||||
/// <param name="dataTypeService">Service responsible for operations related to data types.</param>
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public ChildrenDataTypeTreeController(IEntityService entityService, FlagProviderCollection flagProviders, IDataTypeService dataTypeService)
|
||||
: base(entityService, flagProviders, dataTypeService)
|
||||
{
|
||||
|
||||
+8
-26
@@ -25,26 +25,6 @@ public class DataTypeTreeControllerBase : FolderTreeControllerBase<DataTypeTreeI
|
||||
{
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DataTypeTreeControllerBase"/> class.
|
||||
/// </summary>
|
||||
/// <param name="entityService">Service for managing Umbraco entities.</param>
|
||||
/// <param name="dataTypeService">Service for managing data types within Umbraco.</param>
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")]
|
||||
public DataTypeTreeControllerBase(IEntityService entityService, IDataTypeService dataTypeService)
|
||||
: this(
|
||||
entityService,
|
||||
StaticServiceProvider.Instance.GetRequiredService<FlagProviderCollection>(),
|
||||
dataTypeService)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DataTypeTreeControllerBase"/> class with the specified services.
|
||||
/// </summary>
|
||||
/// <param name="entityService">Service used for entity operations within the data type tree.</param>
|
||||
/// <param name="flagProviders">A collection of providers that supply flags for entities.</param>
|
||||
/// <param name="dataTypeService">Service used for managing data types.</param>
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 19.")]
|
||||
public DataTypeTreeControllerBase(IEntityService entityService, FlagProviderCollection flagProviders, IDataTypeService dataTypeService)
|
||||
: this(
|
||||
@@ -80,17 +60,17 @@ public class DataTypeTreeControllerBase : FolderTreeControllerBase<DataTypeTreeI
|
||||
}
|
||||
}
|
||||
|
||||
protected override DataTypeTreeItemResponseModel[] MapTreeItemViewModels(Guid? parentId, IEntitySlim[] entities)
|
||||
protected override async Task<DataTypeTreeItemResponseModel[]> MapTreeItemViewModelsAsync(Guid? parentId, IEntitySlim[] entities)
|
||||
{
|
||||
Dictionary<int, IDataType> dataTypes = entities.Any()
|
||||
? _dataTypeService
|
||||
.GetAllAsync(entities.Select(entity => entity.Key).ToArray()).GetAwaiter().GetResult()
|
||||
? (await _dataTypeService
|
||||
.GetAllAsync(entities.Select(entity => entity.Key).ToArray()))
|
||||
.ToDictionary(contentType => contentType.Id)
|
||||
: new Dictionary<int, IDataType>();
|
||||
|
||||
return entities.Select(entity =>
|
||||
IEnumerable<Task<DataTypeTreeItemResponseModel>> tasks = entities.Select(async entity =>
|
||||
{
|
||||
DataTypeTreeItemResponseModel responseModel = MapTreeItemViewModel(parentId, entity);
|
||||
DataTypeTreeItemResponseModel responseModel = await MapTreeItemViewModelAsync(parentId, entity);
|
||||
if (dataTypes.TryGetValue(entity.Id, out IDataType? dataType))
|
||||
{
|
||||
responseModel.EditorUiAlias = dataType.EditorUiAlias;
|
||||
@@ -98,6 +78,8 @@ public class DataTypeTreeControllerBase : FolderTreeControllerBase<DataTypeTreeI
|
||||
}
|
||||
|
||||
return responseModel;
|
||||
}).ToArray();
|
||||
});
|
||||
|
||||
return await Task.WhenAll(tasks);
|
||||
}
|
||||
}
|
||||
|
||||
-18
@@ -15,24 +15,6 @@ namespace Umbraco.Cms.Api.Management.Controllers.DataType.Tree;
|
||||
[ApiVersion("1.0")]
|
||||
public class RootDataTypeTreeController : DataTypeTreeControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RootDataTypeTreeController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="entityService">Service used for managing and retrieving entities within Umbraco.</param>
|
||||
/// <param name="dataTypeService">Service used for managing data types in Umbraco.</param>
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")]
|
||||
public RootDataTypeTreeController(IEntityService entityService, IDataTypeService dataTypeService)
|
||||
: base(entityService, dataTypeService)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RootDataTypeTreeController"/> class, which manages the root of the data type tree in the Umbraco management API.
|
||||
/// </summary>
|
||||
/// <param name="entityService">Service used for entity operations within the tree.</param>
|
||||
/// <param name="flagProviders">A collection of providers that supply flags for tree nodes.</param>
|
||||
/// <param name="dataTypeService">Service used for data type management and retrieval.</param>
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public RootDataTypeTreeController(IEntityService entityService, FlagProviderCollection flagProviders, IDataTypeService dataTypeService)
|
||||
: base(entityService, flagProviders, dataTypeService)
|
||||
{
|
||||
|
||||
-18
@@ -13,24 +13,6 @@ namespace Umbraco.Cms.Api.Management.Controllers.DataType.Tree;
|
||||
/// </summary>
|
||||
public class SiblingsDataTypeTreeController : DataTypeTreeControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SiblingsDataTypeTreeController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="entityService">Service used for managing and retrieving entities within Umbraco.</param>
|
||||
/// <param name="dataTypeService">Service used for managing data types in Umbraco.</param>
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")]
|
||||
public SiblingsDataTypeTreeController(IEntityService entityService, IDataTypeService dataTypeService)
|
||||
: base(entityService, dataTypeService)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SiblingsDataTypeTreeController"/> class, which manages operations related to sibling data type trees in the Umbraco CMS.
|
||||
/// </summary>
|
||||
/// <param name="entityService">Service used for entity operations within the CMS.</param>
|
||||
/// <param name="flagProviders">A collection of providers that supply flags for tree nodes.</param>
|
||||
/// <param name="dataTypeService">Service used for managing data types.</param>
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public SiblingsDataTypeTreeController(IEntityService entityService, FlagProviderCollection flagProviders, IDataTypeService dataTypeService)
|
||||
: base(entityService, flagProviders, dataTypeService)
|
||||
{
|
||||
|
||||
@@ -42,7 +42,7 @@ public class ExportDictionaryController : DictionaryControllerBase
|
||||
/// </returns>
|
||||
[HttpGet("{id:guid}/export")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK, MediaTypeNames.Application.Octet)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
[EndpointSummary("Exports a dictionary.")]
|
||||
[EndpointDescription("Exports the dictionary identified by the provided Id to a downloadable format.")]
|
||||
|
||||
-18
@@ -14,24 +14,6 @@ namespace Umbraco.Cms.Api.Management.Controllers.Dictionary.Tree;
|
||||
[ApiVersion("1.0")]
|
||||
public class AncestorsDictionaryTreeController : DictionaryTreeControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AncestorsDictionaryTreeController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="entityService">Service used for managing and retrieving entities within Umbraco.</param>
|
||||
/// <param name="dictionaryItemService">Service used for managing dictionary items in the Umbraco dictionary tree.</param>
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")]
|
||||
public AncestorsDictionaryTreeController(IEntityService entityService, IDictionaryItemService dictionaryItemService)
|
||||
: base(entityService, dictionaryItemService)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AncestorsDictionaryTreeController"/> class, which handles operations related to retrieving ancestor dictionary tree items.
|
||||
/// </summary>
|
||||
/// <param name="entityService">The service used for entity operations.</param>
|
||||
/// <param name="flagProviders">A collection of providers for entity flags.</param>
|
||||
/// <param name="dictionaryItemService">The service used for dictionary item operations.</param>
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public AncestorsDictionaryTreeController(IEntityService entityService, FlagProviderCollection flagProviders, IDictionaryItemService dictionaryItemService)
|
||||
: base(entityService, flagProviders, dictionaryItemService)
|
||||
{
|
||||
|
||||
-18
@@ -16,24 +16,6 @@ namespace Umbraco.Cms.Api.Management.Controllers.Dictionary.Tree;
|
||||
[ApiVersion("1.0")]
|
||||
public class ChildrenDictionaryTreeController : DictionaryTreeControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ChildrenDictionaryTreeController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="entityService">Service used for managing and retrieving entities within the system.</param>
|
||||
/// <param name="dictionaryItemService">Service used for managing dictionary items.</param>
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")]
|
||||
public ChildrenDictionaryTreeController(IEntityService entityService, IDictionaryItemService dictionaryItemService)
|
||||
: base(entityService, dictionaryItemService)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ChildrenDictionaryTreeController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="entityService">Service for managing and retrieving entities within Umbraco.</param>
|
||||
/// <param name="flagProviders">A collection of providers that supply additional flags or metadata for entities.</param>
|
||||
/// <param name="dictionaryItemService">Service for managing dictionary items used for localization.</param>
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public ChildrenDictionaryTreeController(IEntityService entityService, FlagProviderCollection flagProviders, IDictionaryItemService dictionaryItemService)
|
||||
: base(entityService, flagProviders, dictionaryItemService)
|
||||
{
|
||||
|
||||
-22
@@ -1,13 +1,11 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Management.Controllers.Tree;
|
||||
using Umbraco.Cms.Api.Management.Routing;
|
||||
using Umbraco.Cms.Api.Management.Services.Flags;
|
||||
using Umbraco.Cms.Api.Management.ViewModels;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Tree;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
@@ -25,26 +23,6 @@ namespace Umbraco.Cms.Api.Management.Controllers.Dictionary.Tree;
|
||||
// tree controller base. We'll keep it though, in the hope that we can mend EntityService.
|
||||
public class DictionaryTreeControllerBase : NamedEntityTreeControllerBase<NamedEntityTreeItemResponseModel>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DictionaryTreeControllerBase"/> class.
|
||||
/// </summary>
|
||||
/// <param name="entityService">Service used for managing and retrieving entities within the Umbraco system.</param>
|
||||
/// <param name="dictionaryItemService">Service used for managing and retrieving dictionary items for localization.</param>
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")]
|
||||
public DictionaryTreeControllerBase(IEntityService entityService, IDictionaryItemService dictionaryItemService)
|
||||
: this(
|
||||
entityService,
|
||||
StaticServiceProvider.Instance.GetRequiredService<FlagProviderCollection>(),
|
||||
dictionaryItemService)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DictionaryTreeControllerBase"/> class.
|
||||
/// </summary>
|
||||
/// <param name="entityService">Service for managing entities within the system.</param>
|
||||
/// <param name="flagProviders">A collection of providers for entity flags.</param>
|
||||
/// <param name="dictionaryItemService">Service for managing dictionary items.</param>
|
||||
public DictionaryTreeControllerBase(
|
||||
IEntityService entityService,
|
||||
FlagProviderCollection flagProviders,
|
||||
|
||||
+6
-6
@@ -5,6 +5,7 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
using Umbraco.Cms.Api.Management.Services.Flags;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Tree;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
@@ -16,13 +17,12 @@ namespace Umbraco.Cms.Api.Management.Controllers.Dictionary.Tree;
|
||||
[ApiVersion("1.0")]
|
||||
public class RootDictionaryTreeController : DictionaryTreeControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RootDictionaryTreeController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="entityService">Service used for entity operations within the dictionary tree.</param>
|
||||
/// <param name="dictionaryItemService">Service used for managing dictionary items.</param>
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 20.")]
|
||||
public RootDictionaryTreeController(IEntityService entityService, IDictionaryItemService dictionaryItemService)
|
||||
: base(entityService, dictionaryItemService)
|
||||
: this(
|
||||
entityService,
|
||||
StaticServiceProvider.Instance.GetRequiredService<FlagProviderCollection>(),
|
||||
dictionaryItemService)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
-34
@@ -1,13 +1,11 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
using Umbraco.Cms.Api.Management.Services.Flags;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Document.Collection;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Mapping;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
@@ -26,16 +24,6 @@ public class ByKeyDocumentCollectionController : DocumentCollectionControllerBas
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
private readonly IDocumentCollectionPresentationFactory _documentCollectionPresentationFactory;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Umbraco.Cms.Api.Management.Controllers.Document.Collection.ByKeyDocumentCollectionController"/> class,
|
||||
/// which handles document collection operations by document key.
|
||||
/// </summary>
|
||||
/// <param name="contentListViewService">Service for retrieving and managing content list views.</param>
|
||||
/// <param name="backOfficeSecurityAccessor">Accessor for back office security context.</param>
|
||||
/// <param name="mapper">The Umbraco object mapper used for mapping between models.</param>
|
||||
/// <param name="documentCollectionPresentationFactory">Factory for creating document collection presentation models.</param>
|
||||
/// <param name="flagProviders">A collection of providers for document collection flags.</param>
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public ByKeyDocumentCollectionController(
|
||||
IContentListViewService contentListViewService,
|
||||
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
|
||||
@@ -49,28 +37,6 @@ public class ByKeyDocumentCollectionController : DocumentCollectionControllerBas
|
||||
_documentCollectionPresentationFactory = documentCollectionPresentationFactory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Umbraco.Cms.Api.Management.Controllers.Document.Collection.ByKeyDocumentCollectionController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="contentListViewService">Service for managing content list views.</param>
|
||||
/// <param name="backOfficeSecurityAccessor">Accessor for back office security operations.</param>
|
||||
/// <param name="mapper">Maps Umbraco objects to API models.</param>
|
||||
/// <param name="documentCollectionPresentationFactory">Factory for creating document collection presentation models.</param>
|
||||
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 18.")]
|
||||
public ByKeyDocumentCollectionController(
|
||||
IContentListViewService contentListViewService,
|
||||
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
|
||||
IUmbracoMapper mapper,
|
||||
IDocumentCollectionPresentationFactory documentCollectionPresentationFactory)
|
||||
: this(
|
||||
contentListViewService,
|
||||
backOfficeSecurityAccessor,
|
||||
mapper,
|
||||
documentCollectionPresentationFactory,
|
||||
StaticServiceProvider.Instance.GetRequiredService<FlagProviderCollection>())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a paged collection of documents identified by the provided unique identifier.
|
||||
/// </summary>
|
||||
|
||||
@@ -21,6 +21,8 @@ namespace Umbraco.Cms.Api.Management.Controllers.Document;
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocuments)]
|
||||
public abstract class DocumentControllerBase : ContentControllerBase
|
||||
{
|
||||
protected override string EntityName => "document";
|
||||
|
||||
protected IActionResult DocumentNotFound()
|
||||
=> OperationStatusResult(ContentEditingOperationStatus.NotFound, problemDetailsBuilder
|
||||
=> NotFound(problemDetailsBuilder
|
||||
@@ -38,118 +40,7 @@ public abstract class DocumentControllerBase : ContentControllerBase
|
||||
ContentPublishingOperationStatus status,
|
||||
IEnumerable<string>? invalidPropertyAliases = null,
|
||||
IEnumerable<ContentPublishingBranchItemResult>? failedBranchItems = null)
|
||||
=> OperationStatusResult(status, problemDetailsBuilder => status switch
|
||||
{
|
||||
ContentPublishingOperationStatus.ContentNotFound => NotFound(problemDetailsBuilder
|
||||
.WithTitle("The requested document could not be found")
|
||||
.Build()),
|
||||
ContentPublishingOperationStatus.CancelledByEvent => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Publish cancelled by event")
|
||||
.WithDetail("The publish operation was cancelled by an event.")
|
||||
.Build()),
|
||||
ContentPublishingOperationStatus.ContentInvalid => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Invalid document")
|
||||
.WithDetail("The specified document had an invalid configuration.")
|
||||
.WithExtension("invalidProperties", invalidPropertyAliases ?? Enumerable.Empty<string>())
|
||||
.Build()),
|
||||
ContentPublishingOperationStatus.NothingToPublish => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Nothing to publish")
|
||||
.WithDetail("None of the specified cultures needed publishing.")
|
||||
.Build()),
|
||||
ContentPublishingOperationStatus.MandatoryCultureMissing => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Mandatory culture missing")
|
||||
.WithDetail("Must include all mandatory cultures when publishing.")
|
||||
.Build()),
|
||||
ContentPublishingOperationStatus.HasExpired => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Document expired")
|
||||
.WithDetail("Could not publish the document because it was expired.")
|
||||
.Build()),
|
||||
ContentPublishingOperationStatus.CultureHasExpired => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Document culture expired")
|
||||
.WithDetail("Could not publish the document because some of the specified cultures were expired.")
|
||||
.Build()),
|
||||
ContentPublishingOperationStatus.AwaitingRelease => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Document awaiting release")
|
||||
.WithDetail("Could not publish the document because it was awaiting release.")
|
||||
.Build()),
|
||||
ContentPublishingOperationStatus.CultureAwaitingRelease => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Document culture awaiting release")
|
||||
.WithDetail(
|
||||
"Could not publish the document because some of the specified cultures were awaiting release.")
|
||||
.Build()),
|
||||
ContentPublishingOperationStatus.InTrash => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Document in the recycle bin")
|
||||
.WithDetail("Could not publish the document because it was in the recycle bin.")
|
||||
.Build()),
|
||||
ContentPublishingOperationStatus.PathNotPublished => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Parent not published")
|
||||
.WithDetail("Could not publish the document because its parent was not published.")
|
||||
.Build()),
|
||||
ContentPublishingOperationStatus.InvalidCulture => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Invalid cultures specified")
|
||||
.WithDetail("A specified culture is not valid for the operation.")
|
||||
.Build()),
|
||||
ContentPublishingOperationStatus.CultureMissing => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Culture missing")
|
||||
.WithDetail("A culture needs to be specified to execute the operation.")
|
||||
.Build()),
|
||||
ContentPublishingOperationStatus.CannotPublishInvariantWhenVariant => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Cannot publish invariant when variant")
|
||||
.WithDetail("Cannot publish invariant culture when the document varies by culture.")
|
||||
.Build()),
|
||||
ContentPublishingOperationStatus.CannotPublishVariantWhenNotVariant => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Cannot publish variant when not variant.")
|
||||
.WithDetail("Cannot publish a given culture when the document is invariant.")
|
||||
.Build()),
|
||||
ContentPublishingOperationStatus.ConcurrencyViolation => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Concurrency violation detected")
|
||||
.WithDetail("An attempt was made to publish a version older than the latest version.")
|
||||
.Build()),
|
||||
ContentPublishingOperationStatus.UnsavedChanges => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Unsaved changes")
|
||||
.WithDetail(
|
||||
"Could not publish the document because it had unsaved changes. Make sure to save all changes before attempting a publish.")
|
||||
.Build()),
|
||||
ContentPublishingOperationStatus.UnpublishTimeNeedsToBeAfterPublishTime => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Unpublish time needs to be after the publish time")
|
||||
.WithDetail(
|
||||
"Cannot handle an unpublish time that is not after the specified publish time.")
|
||||
.Build()),
|
||||
ContentPublishingOperationStatus.PublishTimeNeedsToBeInFuture => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Publish time needs to be higher than the current time")
|
||||
.WithDetail(
|
||||
"Cannot handle a publish time that is not after the current server time.")
|
||||
.Build()),
|
||||
ContentPublishingOperationStatus.UpublishTimeNeedsToBeInFuture => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Unpublish time needs to be higher than the current time")
|
||||
.WithDetail(
|
||||
"Cannot handle an unpublish time that is not after the current server time.")
|
||||
.Build()),
|
||||
ContentPublishingOperationStatus.CannotUnpublishWhenReferenced => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Cannot unpublish document when it's referenced somewhere else.")
|
||||
.WithDetail(
|
||||
"Cannot unpublish a referenced document, while the setting ContentSettings.DisableUnpublishWhenReferenced is enabled.")
|
||||
.Build()),
|
||||
ContentPublishingOperationStatus.FailedBranch => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Failed branch operation")
|
||||
.WithDetail("One or more items in the branch could not complete the operation.")
|
||||
.WithExtension("failedBranchItems", failedBranchItems?.Select(item => new DocumentPublishBranchItemResult
|
||||
{
|
||||
Id = item.Key,
|
||||
OperationStatus = item.OperationStatus
|
||||
}) ?? Enumerable.Empty<DocumentPublishBranchItemResult>())
|
||||
.Build()),
|
||||
ContentPublishingOperationStatus.Failed => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Publish or unpublish failed")
|
||||
.WithDetail(
|
||||
"An unspecified error occurred while (un)publishing. Please check the logs for additional information.")
|
||||
.Build()),
|
||||
ContentPublishingOperationStatus.TaskResultNotFound => NotFound(problemDetailsBuilder
|
||||
.WithTitle("The result of the submitted task could not be found")
|
||||
.Build()),
|
||||
|
||||
_ => StatusCode(StatusCodes.Status500InternalServerError, "Unknown content operation status."),
|
||||
});
|
||||
=> ContentPublishingOperationStatusResult(status, invalidPropertyAliases, failedBranchItems);
|
||||
|
||||
protected IActionResult PublicAccessOperationStatusResult(PublicAccessOperationStatus status)
|
||||
=> OperationStatusResult(status, problemDetailsBuilder => status switch
|
||||
|
||||
@@ -38,15 +38,6 @@ public class DomainsController : DocumentControllerBase
|
||||
_umbracoMapper = umbracoMapper;
|
||||
}
|
||||
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")]
|
||||
public DomainsController(IDomainService domainService, IUmbracoMapper umbracoMapper)
|
||||
: this(
|
||||
StaticServiceProvider.Instance.GetRequiredService<IAuthorizationService>(),
|
||||
domainService,
|
||||
umbracoMapper)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the list of domains and their associated culture settings assigned to the specified document.
|
||||
/// </summary>
|
||||
|
||||
+2
-3
@@ -1,7 +1,6 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Document.Item;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
@@ -26,7 +25,6 @@ public class ItemDocumentItemController : DocumentItemControllerBase
|
||||
/// </summary>
|
||||
/// <param name="entityService">The service used to manage and retrieve entities within the CMS.</param>
|
||||
/// <param name="documentPresentationFactory">The factory responsible for creating document presentation models.</param>
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public ItemDocumentItemController(
|
||||
IEntityService entityService,
|
||||
IDocumentPresentationFactory documentPresentationFactory)
|
||||
@@ -59,7 +57,8 @@ public class ItemDocumentItemController : DocumentItemControllerBase
|
||||
.GetAll(UmbracoObjectTypes.Document, ids.ToArray())
|
||||
.OfType<IDocumentEntitySlim>();
|
||||
|
||||
IEnumerable<DocumentItemResponseModel> responseModels = documents.Select(_documentPresentationFactory.CreateItemResponseModel);
|
||||
IEnumerable<Task<DocumentItemResponseModel>> tasks = documents.Select(_documentPresentationFactory.CreateItemResponseModelAsync);
|
||||
DocumentItemResponseModel[] responseModels = await Task.WhenAll(tasks);
|
||||
return Ok(responseModels);
|
||||
}
|
||||
}
|
||||
|
||||
+4
-53
@@ -4,7 +4,6 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Document.Item;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Entities;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
@@ -28,7 +27,6 @@ public class SearchDocumentItemController : DocumentItemControllerBase
|
||||
/// <param name="indexedEntitySearchService">Service for searching indexed entities.</param>
|
||||
/// <param name="documentPresentationFactory">Factory for creating document presentation models.</param>
|
||||
/// <param name="dataTypeService">Service for managing data types.</param>
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public SearchDocumentItemController(
|
||||
IIndexedEntitySearchService indexedEntitySearchService,
|
||||
IDocumentPresentationFactory documentPresentationFactory,
|
||||
@@ -39,56 +37,6 @@ public class SearchDocumentItemController : DocumentItemControllerBase
|
||||
_dataTypeService = dataTypeService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SearchDocumentItemController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="indexedEntitySearchService">The service used to perform searches on indexed entities. This dependency is injected.</param>
|
||||
/// <param name="documentPresentationFactory">The factory responsible for creating document presentation models. This dependency is injected.</param>
|
||||
[Obsolete("Use the non-obsolete constructor instead. Scheduled for removal in Umbraco 18.")]
|
||||
public SearchDocumentItemController(
|
||||
IIndexedEntitySearchService indexedEntitySearchService,
|
||||
IDocumentPresentationFactory documentPresentationFactory)
|
||||
: this(
|
||||
indexedEntitySearchService,
|
||||
documentPresentationFactory,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IDataTypeService>())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Searches for document items, including those in the recycle bin, using the specified query and filters.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
|
||||
/// <param name="query">The search query string.</param>
|
||||
/// <param name="trashed">If set, filters results to include only trashed items, only non-trashed items, or both (when null).</param>
|
||||
/// <param name="culture">An optional culture code to filter search results.</param>
|
||||
/// <param name="skip">The number of items to skip (for pagination).</param>
|
||||
/// <param name="take">The maximum number of items to return (for pagination).</param>
|
||||
/// <param name="parentId">An optional parent ID to filter results by parent.</param>
|
||||
/// <param name="allowedDocumentTypes">An optional list of allowed document type IDs to filter results.</param>
|
||||
/// <returns>A task representing the asynchronous operation, with an action result containing the search results.</returns>
|
||||
[Obsolete("Please use the overload taking all parameters. Scheduled for removal in Umbraco 18.")]
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public async Task<IActionResult> SearchWithTrashed(
|
||||
CancellationToken cancellationToken,
|
||||
string query,
|
||||
bool? trashed = null,
|
||||
string? culture = null,
|
||||
int skip = 0,
|
||||
int take = 100,
|
||||
Guid? parentId = null,
|
||||
[FromQuery] IEnumerable<Guid>? allowedDocumentTypes = null)
|
||||
=> await SearchWithTrashed(
|
||||
cancellationToken,
|
||||
query,
|
||||
trashed,
|
||||
culture,
|
||||
skip,
|
||||
take,
|
||||
parentId,
|
||||
allowedDocumentTypes,
|
||||
null);
|
||||
|
||||
/// <summary>
|
||||
/// Searches for document items, including those in the recycle bin, based on the specified query and filters.
|
||||
/// </summary>
|
||||
@@ -130,9 +78,12 @@ public class SearchDocumentItemController : DocumentItemControllerBase
|
||||
take,
|
||||
ignoreUserStartNodes);
|
||||
|
||||
IEnumerable<Task<DocumentItemResponseModel>> tasks = searchResult.Items.OfType<IDocumentEntitySlim>().Select(_documentPresentationFactory.CreateItemResponseModelAsync);
|
||||
DocumentItemResponseModel[] items = await Task.WhenAll(tasks);
|
||||
|
||||
var result = new PagedModel<DocumentItemResponseModel>
|
||||
{
|
||||
Items = searchResult.Items.OfType<IDocumentEntitySlim>().Select(_documentPresentationFactory.CreateItemResponseModel),
|
||||
Items = items,
|
||||
Total = searchResult.Total,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Net.Mime;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
@@ -44,7 +45,7 @@ public class PatchDocumentController : PatchDocumentControllerBase
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)]
|
||||
[EndpointSummary("Make partial updates to a document. For more information, see the documentation at https://docs.umbraco.com/umbraco-cms/reference/management-api/patching/document-endpoint-guide or https://docs.umbraco.com/umbraco-cms/reference/management-api/patching/document-endpoint-spec")]
|
||||
[Consumes("application/json-patch+json")]
|
||||
[Consumes(MediaTypeNames.Application.JsonPatch)]
|
||||
public async Task<IActionResult> Patch(
|
||||
CancellationToken cancellationToken,
|
||||
Guid id,
|
||||
|
||||
+5
-3
@@ -22,6 +22,8 @@ namespace Umbraco.Cms.Api.Management.Controllers.Document.RecycleBin;
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocuments)]
|
||||
public class DocumentRecycleBinControllerBase : RecycleBinControllerBase<DocumentRecycleBinItemResponseModel>
|
||||
{
|
||||
protected override string EntityName => "document";
|
||||
|
||||
private readonly IDocumentPresentationFactory _documentPresentationFactory;
|
||||
|
||||
/// <summary>
|
||||
@@ -37,13 +39,13 @@ public class DocumentRecycleBinControllerBase : RecycleBinControllerBase<Documen
|
||||
|
||||
protected override Guid RecycleBinRootKey => Constants.System.RecycleBinContentKey;
|
||||
|
||||
protected override DocumentRecycleBinItemResponseModel MapRecycleBinViewModel(Guid? parentId, IEntitySlim entity)
|
||||
protected override async Task<DocumentRecycleBinItemResponseModel> MapRecycleBinViewModelAsync(Guid? parentId, IEntitySlim entity)
|
||||
{
|
||||
DocumentRecycleBinItemResponseModel responseModel = base.MapRecycleBinViewModel(parentId, entity);
|
||||
DocumentRecycleBinItemResponseModel responseModel = await base.MapRecycleBinViewModelAsync(parentId, entity);
|
||||
|
||||
if (entity is IDocumentEntitySlim documentEntitySlim)
|
||||
{
|
||||
responseModel.Variants = _documentPresentationFactory.CreateVariantsItemResponseModels(documentEntitySlim);
|
||||
responseModel.Variants = await _documentPresentationFactory.CreateVariantsItemResponseModelsAsync(documentEntitySlim);
|
||||
responseModel.DocumentType = _documentPresentationFactory.CreateDocumentTypeReferenceResponseModel(documentEntitySlim);
|
||||
}
|
||||
|
||||
|
||||
-100
@@ -1,100 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Security.Authorization;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Sorting;
|
||||
using Umbraco.Cms.Core.Actions;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Security.Authorization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Document;
|
||||
|
||||
/// <summary>
|
||||
/// Provides an API endpoint for sorting the root-level documents by a system field.
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
public class SortChildrenAtRootDocumentController : DocumentControllerBase
|
||||
{
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly IContentEditingService _contentEditingService;
|
||||
private readonly IEntityService _entityService;
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SortChildrenAtRootDocumentController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="authorizationService">Service used to authorize user actions.</param>
|
||||
/// <param name="contentEditingService">Service for editing and managing content.</param>
|
||||
/// <param name="entityService">Service used to resolve the children to authorize.</param>
|
||||
/// <param name="backOfficeSecurityAccessor">Accessor for back office security context.</param>
|
||||
public SortChildrenAtRootDocumentController(
|
||||
IAuthorizationService authorizationService,
|
||||
IContentEditingService contentEditingService,
|
||||
IEntityService entityService,
|
||||
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
|
||||
{
|
||||
_authorizationService = authorizationService;
|
||||
_contentEditingService = contentEditingService;
|
||||
_entityService = entityService;
|
||||
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sorts the root-level documents by a system field.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe while waiting for the task to complete.</param>
|
||||
/// <param name="requestModel">The field to sort by and the sort direction.</param>
|
||||
/// <returns>
|
||||
/// An <see cref="IActionResult"/> indicating the outcome of the operation:
|
||||
/// returns <c>200 OK</c> if sorting succeeds or <c>400 Bad Request</c> if the field is not recognised.
|
||||
/// </returns>
|
||||
[HttpPut("root/sort-children")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[EndpointSummary("Sorts the root-level documents by a field.")]
|
||||
[EndpointDescription("Sorts the root-level documents by a system field in the given direction. When sorting by name, an optional culture selects the variant name; the culture is not validated, so documents that do not vary by it (or an unrecognised culture) fall back to the invariant name.")]
|
||||
public async Task<IActionResult> SortChildren(CancellationToken cancellationToken, SortDocumentChildrenByFieldRequestModel requestModel)
|
||||
{
|
||||
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
User,
|
||||
ContentPermissionResource.WithKeys(ActionSort.ActionLetter, (Guid?)null),
|
||||
AuthorizationPolicies.ContentPermissionByResource);
|
||||
|
||||
if (!authorizationResult.Succeeded)
|
||||
{
|
||||
return Forbidden();
|
||||
}
|
||||
|
||||
var childrenAuthorized = await AllChildrenAuthorizer.IsAuthorizedForChildrenAsync(
|
||||
_authorizationService,
|
||||
_entityService,
|
||||
User,
|
||||
parentKey: null,
|
||||
UmbracoObjectTypes.Document,
|
||||
childKeys => ContentPermissionResource.WithKeys(ActionSort.ActionLetter, childKeys),
|
||||
AuthorizationPolicies.ContentPermissionByResource);
|
||||
|
||||
if (!childrenAuthorized)
|
||||
{
|
||||
return Forbidden();
|
||||
}
|
||||
|
||||
ContentEditingOperationStatus result = await _contentEditingService.SortByFieldAsync(
|
||||
null,
|
||||
requestModel.Field,
|
||||
requestModel.Direction,
|
||||
requestModel.Culture,
|
||||
CurrentUserKey(_backOfficeSecurityAccessor));
|
||||
|
||||
return result == ContentEditingOperationStatus.Success
|
||||
? Ok()
|
||||
: ContentEditingOperationStatusResult(result);
|
||||
}
|
||||
}
|
||||
-102
@@ -1,102 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Security.Authorization;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Sorting;
|
||||
using Umbraco.Cms.Core.Actions;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Security.Authorization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Document;
|
||||
|
||||
/// <summary>
|
||||
/// Provides an API endpoint for sorting the children of a document by a system field.
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
public class SortChildrenDocumentController : DocumentControllerBase
|
||||
{
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly IContentEditingService _contentEditingService;
|
||||
private readonly IEntityService _entityService;
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SortChildrenDocumentController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="authorizationService">Service used to authorize user actions.</param>
|
||||
/// <param name="contentEditingService">Service for editing and managing content.</param>
|
||||
/// <param name="entityService">Service used to resolve the children to authorize.</param>
|
||||
/// <param name="backOfficeSecurityAccessor">Accessor for back office security context.</param>
|
||||
public SortChildrenDocumentController(
|
||||
IAuthorizationService authorizationService,
|
||||
IContentEditingService contentEditingService,
|
||||
IEntityService entityService,
|
||||
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
|
||||
{
|
||||
_authorizationService = authorizationService;
|
||||
_contentEditingService = contentEditingService;
|
||||
_entityService = entityService;
|
||||
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sorts the child documents of the specified parent document by a system field.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe while waiting for the task to complete.</param>
|
||||
/// <param name="id">The unique identifier of the parent document whose children should be sorted.</param>
|
||||
/// <param name="requestModel">The field to sort by and the sort direction.</param>
|
||||
/// <returns>
|
||||
/// An <see cref="IActionResult"/> indicating the outcome of the operation:
|
||||
/// returns <c>200 OK</c> if sorting succeeds, <c>400 Bad Request</c> if the field is not recognised, or <c>404 Not Found</c> if the parent document does not exist.
|
||||
/// </returns>
|
||||
[HttpPut("{id:guid}/sort-children")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
[EndpointSummary("Sorts the children of a document by a field.")]
|
||||
[EndpointDescription("Sorts the children of the specified parent document by a system field in the given direction. When sorting by name, an optional culture selects the variant name; the culture is not validated, so children that do not vary by it (or an unrecognised culture) fall back to the invariant name.")]
|
||||
public async Task<IActionResult> SortChildren(CancellationToken cancellationToken, Guid id, SortDocumentChildrenByFieldRequestModel requestModel)
|
||||
{
|
||||
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
User,
|
||||
ContentPermissionResource.WithKeys(ActionSort.ActionLetter, id),
|
||||
AuthorizationPolicies.ContentPermissionByResource);
|
||||
|
||||
if (!authorizationResult.Succeeded)
|
||||
{
|
||||
return Forbidden();
|
||||
}
|
||||
|
||||
var childrenAuthorized = await AllChildrenAuthorizer.IsAuthorizedForChildrenAsync(
|
||||
_authorizationService,
|
||||
_entityService,
|
||||
User,
|
||||
id,
|
||||
UmbracoObjectTypes.Document,
|
||||
childKeys => ContentPermissionResource.WithKeys(ActionSort.ActionLetter, childKeys),
|
||||
AuthorizationPolicies.ContentPermissionByResource);
|
||||
|
||||
if (!childrenAuthorized)
|
||||
{
|
||||
return Forbidden();
|
||||
}
|
||||
|
||||
ContentEditingOperationStatus result = await _contentEditingService.SortByFieldAsync(
|
||||
id,
|
||||
requestModel.Field,
|
||||
requestModel.Direction,
|
||||
requestModel.Culture,
|
||||
CurrentUserKey(_backOfficeSecurityAccessor));
|
||||
|
||||
return result == ContentEditingOperationStatus.Success
|
||||
? Ok()
|
||||
: ContentEditingOperationStatusResult(result);
|
||||
}
|
||||
}
|
||||
-41
@@ -20,7 +20,6 @@ namespace Umbraco.Cms.Api.Management.Controllers.Document.Tree;
|
||||
[ApiVersion("1.0")]
|
||||
public class AncestorsDocumentTreeController : DocumentTreeControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AncestorsDocumentTreeController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="entityService">Service for managing and retrieving entities in the system.</param>
|
||||
@@ -83,46 +82,6 @@ public class AncestorsDocumentTreeController : DocumentTreeControllerBase
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Umbraco.Cms.Api.Management.Controllers.Document.Tree.AncestorsDocumentTreeController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="entityService">Service used for entity operations within the Umbraco CMS.</param>
|
||||
/// <param name="userStartNodeEntitiesService">Service for resolving user start nodes for entities.</param>
|
||||
/// <param name="dataTypeService">Service for managing data types in the CMS.</param>
|
||||
/// <param name="publicAccessService">Service for handling public access permissions on content.</param>
|
||||
/// <param name="appCaches">Provides access to application-level caches.</param>
|
||||
/// <param name="backofficeSecurityAccessor">Accessor for backoffice security context and operations.</param>
|
||||
/// <param name="documentPresentationFactory">Factory for creating document presentation models.</param>
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")]
|
||||
public AncestorsDocumentTreeController(
|
||||
IEntityService entityService,
|
||||
IUserStartNodeEntitiesService userStartNodeEntitiesService,
|
||||
IDataTypeService dataTypeService,
|
||||
IPublicAccessService publicAccessService,
|
||||
AppCaches appCaches,
|
||||
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
|
||||
IDocumentPresentationFactory documentPresentationFactory)
|
||||
: base(
|
||||
entityService,
|
||||
userStartNodeEntitiesService,
|
||||
dataTypeService,
|
||||
publicAccessService,
|
||||
appCaches,
|
||||
backofficeSecurityAccessor,
|
||||
documentPresentationFactory)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AncestorsDocumentTreeController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="entityService">Service for managing and retrieving entities within Umbraco.</param>
|
||||
/// <param name="flagProviders">A collection of providers for handling entity flags.</param>
|
||||
/// <param name="userStartNodeEntitiesService">Service for resolving user start node entities.</param>
|
||||
/// <param name="dataTypeService">Service for managing data types in Umbraco.</param>
|
||||
/// <param name="publicAccessService">Service for handling public access permissions.</param>
|
||||
/// <param name="appCaches">Provides access to application-level caches.</param>
|
||||
/// <param name="backofficeSecurityAccessor">Accessor for backoffice security context and operations.</param>
|
||||
/// <param name="documentPresentationFactory">Factory for creating document presentation models.</param>
|
||||
[Obsolete("Please use the constructor accepting IDocumentStartNodeTreeFilterService. Scheduled for removal in Umbraco 19.")]
|
||||
public AncestorsDocumentTreeController(
|
||||
IEntityService entityService,
|
||||
|
||||
-41
@@ -21,10 +21,6 @@ namespace Umbraco.Cms.Api.Management.Controllers.Document.Tree;
|
||||
[ApiVersion("1.0")]
|
||||
public class ChildrenDocumentTreeController : DocumentTreeControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ChildrenDocumentTreeController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="entityService">Service for managing and retrieving entities in the system.</param>
|
||||
/// <param name="flagProviders">A collection of providers that supply flags for document tree nodes.</param>
|
||||
/// <param name="treeFilterService">Service for filtering document tree entities based on user start nodes.</param>
|
||||
/// <param name="publicAccessService">Service for handling public access permissions on documents.</param>
|
||||
@@ -87,43 +83,6 @@ public class ChildrenDocumentTreeController : DocumentTreeControllerBase
|
||||
/// Initializes a new instance of the <see cref="ChildrenDocumentTreeController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="entityService">Service for managing and retrieving entities in the system.</param>
|
||||
/// <param name="userStartNodeEntitiesService">Service for resolving user start nodes for entities.</param>
|
||||
/// <param name="dataTypeService">Service for accessing and managing data types.</param>
|
||||
/// <param name="publicAccessService">Service for handling public access permissions and restrictions.</param>
|
||||
/// <param name="appCaches">Provides application-level caching mechanisms.</param>
|
||||
/// <param name="backofficeSecurityAccessor">Accessor for back office security context and operations.</param>
|
||||
/// <param name="documentPresentationFactory">Factory for creating document presentation models.</param>
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")]
|
||||
public ChildrenDocumentTreeController(
|
||||
IEntityService entityService,
|
||||
IUserStartNodeEntitiesService userStartNodeEntitiesService,
|
||||
IDataTypeService dataTypeService,
|
||||
IPublicAccessService publicAccessService,
|
||||
AppCaches appCaches,
|
||||
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
|
||||
IDocumentPresentationFactory documentPresentationFactory)
|
||||
: base(
|
||||
entityService,
|
||||
userStartNodeEntitiesService,
|
||||
dataTypeService,
|
||||
publicAccessService,
|
||||
appCaches,
|
||||
backofficeSecurityAccessor,
|
||||
documentPresentationFactory)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ChildrenDocumentTreeController"/> class, responsible for managing child document tree operations in the Umbraco backoffice API.
|
||||
/// </summary>
|
||||
/// <param name="entityService">Service for accessing and managing entities within Umbraco.</param>
|
||||
/// <param name="flagProviders">A collection of providers that supply flags for document tree nodes.</param>
|
||||
/// <param name="userStartNodeEntitiesService">Service for resolving user-specific start nodes in the content tree.</param>
|
||||
/// <param name="dataTypeService">Service for managing data types in Umbraco.</param>
|
||||
/// <param name="publicAccessService">Service for handling public access permissions on documents.</param>
|
||||
/// <param name="appCaches">Provides application-level caching functionality.</param>
|
||||
/// <param name="backofficeSecurityAccessor">Accessor for backoffice security context and authentication.</param>
|
||||
/// <param name="documentPresentationFactory">Factory for creating document presentation models for the API.</param>
|
||||
[Obsolete("Please use the constructor accepting IDocumentStartNodeTreeFilterService. Scheduled for removal in Umbraco 19.")]
|
||||
public ChildrenDocumentTreeController(
|
||||
IEntityService entityService,
|
||||
|
||||
+3
-24
@@ -37,27 +37,6 @@ public abstract class DocumentTreeControllerBase : UserStartNodeTreeControllerBa
|
||||
private readonly AppCaches? _appCaches;
|
||||
private readonly IBackOfficeSecurityAccessor? _backofficeSecurityAccessor;
|
||||
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")]
|
||||
protected DocumentTreeControllerBase(
|
||||
IEntityService entityService,
|
||||
IUserStartNodeEntitiesService userStartNodeEntitiesService,
|
||||
IDataTypeService dataTypeService,
|
||||
IPublicAccessService publicAccessService,
|
||||
AppCaches appCaches,
|
||||
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
|
||||
IDocumentPresentationFactory documentPresentationFactory)
|
||||
: this(
|
||||
entityService,
|
||||
StaticServiceProvider.Instance.GetRequiredService<FlagProviderCollection>(),
|
||||
userStartNodeEntitiesService,
|
||||
dataTypeService,
|
||||
publicAccessService,
|
||||
appCaches,
|
||||
backofficeSecurityAccessor,
|
||||
documentPresentationFactory)
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("Please use the constructor accepting IDocumentStartNodeTreeFilterService. Scheduled for removal in Umbraco 19.")]
|
||||
protected DocumentTreeControllerBase(
|
||||
IEntityService entityService,
|
||||
@@ -129,9 +108,9 @@ public abstract class DocumentTreeControllerBase : UserStartNodeTreeControllerBa
|
||||
|
||||
protected override Ordering ItemOrdering => Ordering.By(Infrastructure.Persistence.Dtos.NodeDto.SortOrderColumnName);
|
||||
|
||||
protected override DocumentTreeItemResponseModel MapTreeItemViewModel(Guid? parentId, IEntitySlim entity)
|
||||
protected override async Task<DocumentTreeItemResponseModel> MapTreeItemViewModelAsync(Guid? parentId, IEntitySlim entity)
|
||||
{
|
||||
DocumentTreeItemResponseModel responseModel = base.MapTreeItemViewModel(parentId, entity);
|
||||
DocumentTreeItemResponseModel responseModel = await base.MapTreeItemViewModelAsync(parentId, entity);
|
||||
|
||||
if (entity is IDocumentEntitySlim documentEntitySlim)
|
||||
{
|
||||
@@ -142,7 +121,7 @@ public abstract class DocumentTreeControllerBase : UserStartNodeTreeControllerBa
|
||||
responseModel.Id = entity.Key;
|
||||
responseModel.CreateDate = entity.CreateDate;
|
||||
|
||||
responseModel.Variants = _documentPresentationFactory.CreateVariantsItemResponseModels(documentEntitySlim);
|
||||
responseModel.Variants = await _documentPresentationFactory.CreateVariantsItemResponseModelsAsync(documentEntitySlim);
|
||||
responseModel.DocumentType = _documentPresentationFactory.CreateDocumentTypeReferenceResponseModel(documentEntitySlim);
|
||||
}
|
||||
|
||||
|
||||
-41
@@ -21,7 +21,6 @@ namespace Umbraco.Cms.Api.Management.Controllers.Document.Tree;
|
||||
[ApiVersion("1.0")]
|
||||
public class RootDocumentTreeController : DocumentTreeControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RootDocumentTreeController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="entityService">Service for managing and retrieving entities in the system.</param>
|
||||
@@ -84,46 +83,6 @@ public class RootDocumentTreeController : DocumentTreeControllerBase
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RootDocumentTreeController"/> class, which manages the root nodes of the document tree in the Umbraco backoffice.
|
||||
/// </summary>
|
||||
/// <param name="entityService">The service used for entity operations.</param>
|
||||
/// <param name="userStartNodeEntitiesService">The service for resolving user start node entities.</param>
|
||||
/// <param name="dataTypeService">The service for managing data types.</param>
|
||||
/// <param name="publicAccessService">The service for handling public access permissions.</param>
|
||||
/// <param name="appCaches">The application-level caches.</param>
|
||||
/// <param name="backofficeSecurityAccessor">Accessor for backoffice security context.</param>
|
||||
/// <param name="documentPresentationFactory">Factory for creating document presentation models.</param>
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")]
|
||||
public RootDocumentTreeController(
|
||||
IEntityService entityService,
|
||||
IUserStartNodeEntitiesService userStartNodeEntitiesService,
|
||||
IDataTypeService dataTypeService,
|
||||
IPublicAccessService publicAccessService,
|
||||
AppCaches appCaches,
|
||||
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
|
||||
IDocumentPresentationFactory documentPresentationFactory)
|
||||
: base(
|
||||
entityService,
|
||||
userStartNodeEntitiesService,
|
||||
dataTypeService,
|
||||
publicAccessService,
|
||||
appCaches,
|
||||
backofficeSecurityAccessor,
|
||||
documentPresentationFactory)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Umbraco.Cms.Api.Management.Controllers.Document.Tree.RootDocumentTreeController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="entityService">Service for managing and retrieving entities within Umbraco.</param>
|
||||
/// <param name="flagProviders">A collection of providers that supply flags for document tree nodes.</param>
|
||||
/// <param name="userStartNodeEntitiesService">Service for resolving user-specific start nodes in the content tree.</param>
|
||||
/// <param name="dataTypeService">Service for accessing and managing data types.</param>
|
||||
/// <param name="publicAccessService">Service for handling public access permissions on documents.</param>
|
||||
/// <param name="appCaches">Provides application-level caching mechanisms.</param>
|
||||
/// <param name="backofficeSecurityAccessor">Accessor for backoffice security context and operations.</param>
|
||||
/// <param name="documentPresentationFactory">Factory for creating document presentation models.</param>
|
||||
[Obsolete("Please use the constructor accepting IDocumentStartNodeTreeFilterService. Scheduled for removal in Umbraco 19.")]
|
||||
public RootDocumentTreeController(
|
||||
IEntityService entityService,
|
||||
|
||||
-41
@@ -21,10 +21,6 @@ namespace Umbraco.Cms.Api.Management.Controllers.Document.Tree;
|
||||
[ApiVersion("1.0")]
|
||||
public class SiblingsDocumentTreeController : DocumentTreeControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SiblingsDocumentTreeController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="entityService">Service for managing and retrieving entities in the system.</param>
|
||||
/// <param name="flagProviders">A collection of providers that supply flags for document tree nodes.</param>
|
||||
/// <param name="treeFilterService">Service for filtering document tree entities based on user start nodes.</param>
|
||||
/// <param name="publicAccessService">Service for handling public access permissions on documents.</param>
|
||||
@@ -87,43 +83,6 @@ public class SiblingsDocumentTreeController : DocumentTreeControllerBase
|
||||
/// Initializes a new instance of the <see cref="SiblingsDocumentTreeController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="entityService">Service for managing and retrieving entities in the system.</param>
|
||||
/// <param name="userStartNodeEntitiesService">Service for resolving user start nodes for entities.</param>
|
||||
/// <param name="dataTypeService">Service for accessing and managing data types.</param>
|
||||
/// <param name="publicAccessService">Service for handling public access permissions.</param>
|
||||
/// <param name="appCaches">Provides application-level caching functionality.</param>
|
||||
/// <param name="backofficeSecurityAccessor">Accessor for backoffice security context and operations.</param>
|
||||
/// <param name="documentPresentationFactory">Factory for creating document presentation models.</param>
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")]
|
||||
public SiblingsDocumentTreeController(
|
||||
IEntityService entityService,
|
||||
IUserStartNodeEntitiesService userStartNodeEntitiesService,
|
||||
IDataTypeService dataTypeService,
|
||||
IPublicAccessService publicAccessService,
|
||||
AppCaches appCaches,
|
||||
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
|
||||
IDocumentPresentationFactory documentPresentationFactory)
|
||||
: base(
|
||||
entityService,
|
||||
userStartNodeEntitiesService,
|
||||
dataTypeService,
|
||||
publicAccessService,
|
||||
appCaches,
|
||||
backofficeSecurityAccessor,
|
||||
documentPresentationFactory)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SiblingsDocumentTreeController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="entityService">Service used for entity operations within the Umbraco CMS.</param>
|
||||
/// <param name="flagProviders">A collection of providers that supply flag information for entities.</param>
|
||||
/// <param name="userStartNodeEntitiesService">Service for resolving user start node entities.</param>
|
||||
/// <param name="dataTypeService">Service for managing data types in the CMS.</param>
|
||||
/// <param name="publicAccessService">Service for handling public access permissions.</param>
|
||||
/// <param name="appCaches">Provides access to application-level caches.</param>
|
||||
/// <param name="backofficeSecurityAccessor">Accessor for backoffice security context and operations.</param>
|
||||
/// <param name="documentPresentationFactory">Factory for creating document presentation models.</param>
|
||||
[Obsolete("Please use the constructor accepting IDocumentStartNodeTreeFilterService. Scheduled for removal in Umbraco 19.")]
|
||||
public SiblingsDocumentTreeController(
|
||||
IEntityService entityService,
|
||||
|
||||
@@ -44,16 +44,6 @@ public class UpdateDomainsController : DocumentControllerBase
|
||||
_domainPresentationFactory = domainPresentationFactory;
|
||||
}
|
||||
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")]
|
||||
public UpdateDomainsController(IDomainService domainService, IUmbracoMapper umbracoMapper, IDomainPresentationFactory domainPresentationFactory)
|
||||
: this(
|
||||
StaticServiceProvider.Instance.GetRequiredService<IAuthorizationService>(),
|
||||
domainService,
|
||||
umbracoMapper,
|
||||
domainPresentationFactory)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the domains assigned to the specified document.
|
||||
/// </summary>
|
||||
|
||||
@@ -41,16 +41,6 @@ public class UpdateNotificationsController : DocumentControllerBase
|
||||
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
|
||||
}
|
||||
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")]
|
||||
public UpdateNotificationsController(IContentEditingService contentEditingService, INotificationService notificationService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
|
||||
: this(
|
||||
StaticServiceProvider.Instance.GetRequiredService<IAuthorizationService>(),
|
||||
contentEditingService,
|
||||
notificationService,
|
||||
backOfficeSecurityAccessor)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the notification subscriptions for the current user on the specified document.
|
||||
/// </summary>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user