Files
Umbraco-CMS/src/Umbraco.Cms.Api.Delivery/CLAUDE.md

383 lines
12 KiB
Markdown
Raw Normal View History

# Umbraco.Cms.Api.Delivery
Headless content delivery REST API for Umbraco CMS. Enables frontend applications to fetch published content, media, and member-protected resources.
---
## 1. Architecture
**Type**: Class Library (NuGet Package)
**Target Framework**: .NET 10.0
**Purpose**: Content Delivery API for headless CMS scenarios
### Key Technologies
- **ASP.NET Core** - Web framework
- **OpenIddict** - Member authentication (OAuth 2.0)
- **Asp.Versioning** - API versioning (V1, V2)
- **Output Caching** - Configurable response caching
- **Examine/Lucene** - Content querying
### Dependencies
- `Umbraco.Cms.Api.Common` - Shared API infrastructure (OpenAPI, auth)
- `Umbraco.Web.Common` - Web functionality
### Project Structure (86 files)
```
Umbraco.Cms.Api.Delivery/
├── Controllers/
│ ├── Content/ # Content endpoints (by ID, route, query)
│ ├── Media/ # Media endpoints (by ID, path, query)
│ └── Security/ # Member auth (authorize, token, signout)
├── Querying/
│ ├── Filters/ # ContentType, Name, CreateDate, UpdateDate
│ ├── Selectors/ # Ancestors, Children, Descendants
│ └── Sorts/ # Name, CreateDate, UpdateDate, Level, SortOrder
├── Indexing/ # Lucene index field handlers
├── Services/ # Business logic and query building
├── Caching/ # Output cache policies
├── Rendering/ # Output expansion strategies
├── Configuration/ # Swagger configuration
└── Filters/ # Action filters (access, validation)
```
### Design Patterns
1. **Strategy Pattern** - Query handlers (`ISelectorHandler`, `IFilterHandler`, `ISortHandler`)
2. **Factory Pattern** - `ApiContentQueryFactory` builds Examine queries
3. **Template Method** - `ContentApiControllerBase` for shared controller logic
4. **Options Pattern** - `DeliveryApiSettings` for all configuration
---
## 2. Commands
See "Quick Reference" section at bottom for common commands.
---
## 3. Key Patterns
### API Versioning (V1 vs V2)
**V1** (legacy) and **V2** (current) coexist. Key difference is output expansion:
```csharp
// DependencyInjection/UmbracoBuilderExtensions.cs:49-52
// V1 uses RequestContextOutputExpansionStrategy
// V2+ uses RequestContextOutputExpansionStrategyV2
return apiVersion.MajorVersion == 1
? provider.GetRequiredService<RequestContextOutputExpansionStrategy>()
: provider.GetRequiredService<RequestContextOutputExpansionStrategyV2>();
```
**Why V2**: Improved `expand` and `fields` query parameter parsing (tree-based).
### Query System Architecture
Content querying flows through handlers registered in DI:
1. **Selectors** (`fetch` parameter): `ancestors:id`, `children:id`, `descendants:id`
2. **Filters** (`filter[]` parameter): `contentType:alias`, `name:value`, `createDate>2024-01-01`
3. **Sorts** (`sort[]` parameter): `name:asc`, `createDate:desc`, `level:asc`
```csharp
// Services/ApiContentQueryService.cs:91-96
ISelectorHandler? selectorHandler = _selectorHandlers.FirstOrDefault(h => h.CanHandle(fetch));
return selectorHandler?.BuildSelectorOption(fetch);
```
### Path Decoding Workaround
ASP.NET Core doesn't decode forward slashes in route parameters:
```csharp
// Controllers/DeliveryApiControllerBase.cs:21-31
// OpenAPI clients URL-encode paths, but ASP.NET Core doesn't decode "/"
// See https://github.com/dotnet/aspnetcore/issues/11544
if (path.Contains("%2F", StringComparison.OrdinalIgnoreCase))
{
path = WebUtility.UrlDecode(path);
}
```
---
## 4. Testing
**Location**: No direct tests - tested via integration tests in test projects
```bash
dotnet test tests/Umbraco.Tests.Integration/ --filter "FullyQualifiedName~Delivery"
```
**Internals exposed to** (csproj lines 28-36):
- `Umbraco.Tests.UnitTests`
- `Umbraco.Tests.Integration`
- `DynamicProxyGenAssembly2` (for mocking)
**Focus areas**:
- Query parsing (selectors, filters, sorts)
- Member authentication flows
- Output caching behavior
- Protected content access
---
## 5. Security & Access Control
### Three Access Modes
```csharp
// Services/ApiAccessService.cs:21-27
public bool HasPublicAccess() => _deliveryApiSettings.PublicAccess || HasValidApiKey();
public bool HasPreviewAccess() => HasValidApiKey();
public bool HasMediaAccess() => _deliveryApiSettings is { PublicAccess: true, Media.PublicAccess: true } || HasValidApiKey();
```
**Access levels**:
1. **Public** - No authentication required (if enabled)
2. **API Key** - Via `Api-Key` header
3. **Preview** - Always requires API key
### Member Authentication
OpenIddict-based OAuth 2.0 for member-protected content:
**Flows supported** (Controllers/Security/MemberController.cs):
- Authorization Code + PKCE (line 53)
- Client Credentials (line 112)
- Refresh Token (line 98)
**Endpoints**:
- `GET /umbraco/delivery/api/v1/security/member/authorize`
- `POST /umbraco/delivery/api/v1/security/member/token`
- `GET /umbraco/delivery/api/v1/security/member/signout`
**Scopes**: Only `openid` and `offline_access` allowed for members (line 220-222)
### Protected Content
Member access checked via `ProtectedAccess` model:
```csharp
// Controllers/Content/QueryContentApiController.cs:56-57
ProtectedAccess protectedAccess = await _requestMemberAccessService.MemberAccessAsync();
Attempt<PagedModel<Guid>, ApiContentQueryOperationStatus> queryAttempt =
_apiContentQueryService.ExecuteQuery(fetch, filter, sort, protectedAccess, skip, take);
```
---
## 6. Output Caching
### Cache Policy Configuration
```csharp
// DependencyInjection/UmbracoBuilderExtensions.cs:120-136
// Content and Media have separate cache durations
options.AddPolicy(
Constants.DeliveryApi.OutputCache.ContentCachePolicy,
new DeliveryApiOutputCachePolicy(
outputCacheSettings.ContentDuration,
new StringValues([AcceptLanguage, AcceptSegment, StartItem])));
```
**Cache invalidation conditions** (Caching/DeliveryApiOutputCachePolicy.cs:31):
```csharp
// Never cache preview or non-public access
context.EnableOutputCaching = requestPreviewService.IsPreview() is false
&& apiAccessService.HasPublicAccess();
```
**Vary by headers**: `Accept-Language`, `Accept-Segment`, `Start-Item`
---
## 7. Edge Cases & Known Issues
### Technical Debt (TODOs in codebase)
1. **V1 Removal Pending** (4 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
- `CS0618/CS0612` - Obsolete member references
### Empty Query Results
Query service returns empty results (not errors) for invalid options:
```csharp
// Services/ApiContentQueryService.cs:54-78
// Invalid selector/filter/sort returns fail status with empty result
return Attempt.FailWithStatus(ApiContentQueryOperationStatus.SelectorOptionNotFound, emptyResult);
```
### Start Item Fallback
When no `fetch` parameter provided, uses start item or all content:
```csharp
// Services/ApiContentQueryService.cs:99-112
if (_requestStartItemProviderAccessor.TryGetValue(out IRequestStartItemProvider? requestStartItemProvider))
{
IPublishedContent? startItem = requestStartItemProvider.GetStartItem();
// Use descendants of start item
}
return _apiContentQueryProvider.AllContentSelectorOption(); // Fallback to all
```
---
## 8. Project-Specific Notes
### V1 vs V2 Differences
| Feature | V1 | V2 |
|---------|----|----|
| Output expansion | Basic | Tree-based parsing |
| `expand` parameter | Flat list | Nested syntax |
| `fields` parameter | Limited | Full property selection |
| Default expansion strategy | `RequestContextOutputExpansionStrategy` | `RequestContextOutputExpansionStrategyV2` |
**Migration note**: V1 is deprecated; plan removal when V17+ drops V1 support.
### JSON Configuration
Delivery API has its own JSON options (distinct from Management API):
```csharp
// DependencyInjection/UmbracoBuilderExtensions.cs:82-88
.AddJsonOptions(Constants.JsonOptionsNames.DeliveryApi, options =>
{
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
options.JsonSerializerOptions.TypeInfoResolver = new DeliveryApiJsonTypeResolver();
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
});
```
### Member Token Revocation
Tokens automatically revoked on member changes:
```csharp
// DependencyInjection/UmbracoBuilderExtensions.cs:93-96
builder.AddNotificationAsyncHandler<MemberSavedNotification, RevokeMemberAuthenticationTokensNotificationHandler>();
builder.AddNotificationAsyncHandler<MemberDeletedNotification, RevokeMemberAuthenticationTokensNotificationHandler>();
builder.AddNotificationAsyncHandler<AssignedMemberRolesNotification, RevokeMemberAuthenticationTokensNotificationHandler>();
builder.AddNotificationAsyncHandler<RemovedMemberRolesNotification, RevokeMemberAuthenticationTokensNotificationHandler>();
```
### External Dependencies
**Examine/Lucene** (via Core):
- Powers content querying
- Selector/Filter/Sort handlers build Lucene queries
**OpenIddict** (via Api.Common):
- Member OAuth 2.0 authentication
- Reference tokens (not JWT)
### Configuration (appsettings.json)
```json
{
"Umbraco": {
"CMS": {
"DeliveryApi": {
"Enabled": true,
"PublicAccess": true,
"ApiKey": "your-api-key",
"Media": {
"Enabled": true,
"PublicAccess": true
},
"MemberAuthorization": {
"AuthorizationCodeFlow": { "Enabled": true },
"ClientCredentialsFlow": { "Enabled": false }
},
"OutputCache": {
"Enabled": true,
"ContentDuration": "00:01:00",
"MediaDuration": "00:01:00"
}
}
}
}
}
```
### API Endpoints Summary
**Content** (`/umbraco/delivery/api/v2/content`):
- `GET /item/{id}` - Single content by GUID
- `GET /item/{path}` - Single content by route
- `GET /items` - Multiple by IDs
- `GET /` - Query with fetch/filter/sort
**Media** (`/umbraco/delivery/api/v2/media`):
- `GET /item/{id}` - Single media by GUID
- `GET /item/{path}` - Single media by path
- `GET /items` - Multiple by IDs
- `GET /` - Query media
**Security** (`/umbraco/delivery/api/v1/security/member`):
- `GET /authorize` - Start OAuth flow
- `POST /token` - Exchange code for token
- `GET /signout` - Revoke session
---
## Quick Reference
### Essential Commands
```bash
# Build project
dotnet build src/Umbraco.Cms.Api.Delivery/Umbraco.Cms.Api.Delivery.csproj
# Pack for NuGet
dotnet pack src/Umbraco.Cms.Api.Delivery/Umbraco.Cms.Api.Delivery.csproj -c Release
# Run integration tests
dotnet test tests/Umbraco.Tests.Integration/ --filter "FullyQualifiedName~Delivery"
# Check packages
dotnet list src/Umbraco.Cms.Api.Delivery/Umbraco.Cms.Api.Delivery.csproj package --outdated
```
### Key Classes
| Class | Purpose | File |
|-------|---------|------|
| `DeliveryApiControllerBase` | Base controller with path decoding | Controllers/DeliveryApiControllerBase.cs |
| `ApiContentQueryService` | Query orchestration | Services/ApiContentQueryService.cs |
| `ApiAccessService` | Access control logic | Services/ApiAccessService.cs |
| `DeliveryApiOutputCachePolicy` | Cache policy implementation | Caching/DeliveryApiOutputCachePolicy.cs |
| `MemberController` | OAuth endpoints | Controllers/Security/MemberController.cs |
| `RequestContextOutputExpansionStrategyV2` | V2 output expansion | Rendering/RequestContextOutputExpansionStrategyV2.cs |
### Important Files
- `Umbraco.Cms.Api.Delivery.csproj` - Project dependencies
- `DependencyInjection/UmbracoBuilderExtensions.cs` - DI registration (lines 33-141)
- `Configuration/DeliveryApiConfiguration.cs` - API constants
- `Services/ApiContentQueryService.cs` - Query execution
### Getting Help
- **Root documentation**: `/CLAUDE.md` - Repository overview
- **API Common patterns**: `/src/Umbraco.Cms.Api.Common/CLAUDE.md`
- **Official docs**: https://docs.umbraco.com/umbraco-cms/reference/content-delivery-api
- **Media docs**: https://docs.umbraco.com/umbraco-cms/reference/content-delivery-api/media-delivery-api
---
**This library exposes Umbraco content and media via REST for headless scenarios. Focus on query handlers, access control, and member authentication when working here.**