# 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.**
