File: CLAUDE.md
Umbraco CMS - Multi-Project Repository
Enterprise-grade CMS built on .NET 10.0. This repository contains 21 production projects organized in a layered architecture with clear separation of concerns.
Repository: https://github.com/umbraco/Umbraco-CMS
License: MIT
Main Branch: main
1. Overview
What This Repository Contains
21 Production Projects organized in 3 main categories:
Core Architecture (Domain & Infrastructure)
Umbraco.Core- Interface contracts, domain models, notificationsUmbraco.Infrastructure- Service implementations, data access, caching
Web & APIs (Presentation Layer)
Umbraco.Web.UI- Main ASP.NET Core web applicationUmbraco.Web.Common- Shared web functionality, controllers, middlewareUmbraco.Cms.Api.Management- Backoffice Management API (REST)Umbraco.Cms.Api.Delivery- Content Delivery API (headless)Umbraco.Cms.Api.Common- Shared API infrastructure
Specialized Features (Pluggable Modules)
- Persistence: EF Core (modern), NPoco (legacy) for SQL Server & SQLite
- Caching:
PublishedCache.HybridCache(in-memory + distributed) - Search:
Examine.Lucene(full-text search) - Imaging:
Imaging.ImageSharpv1 & v2 (image processing) - Other: Static assets, targets, development tools
6 Test Projects:
Umbraco.Tests.Common- Shared test utilitiesUmbraco.Tests.UnitTests- Unit testsUmbraco.Tests.Integration- Integration testsUmbraco.Tests.Benchmarks- Performance benchmarksUmbraco.Tests.AcceptanceTest- E2E testsUmbraco.Tests.AcceptanceTest.UmbracoProject- Test instance
Key Technologies
- .NET 10.0 - Target framework for all projects
- ASP.NET Core - Web framework
- Entity Framework Core - Modern ORM
- OpenIddict - OAuth 2.0/OpenID Connect authentication
- Microsoft.AspNetCore.OpenApi - OpenAPI document generation
- Swashbuckle.AspNetCore.SwaggerUI - Swagger UI for API documentation
- Lucene.NET - Full-text search via Examine
- ImageSharp - Image processing
2. General-Purpose by Default
This repository is a product platform, not an application. Every layer is consumed by code that does not live here — implementors, package developers, and other parts of the CMS. A change is finished when it serves the use case that prompted it and the use cases nobody has described yet.
Design to the contract. When you change shared code, work out the rule the layer must uphold for any implementation of it, and make that rule hold there. Name a concrete implementation freely in the code that owns it — a specific service, package, or project; a shared or generic layer implements only the contract. A specific editor alias, content type alias, or class name appearing in generic code is the signal that a fix has been fitted to one caller — express it instead as a capability the contract exposes.
Describe the contract. Comments and public docs use the vocabulary of the layer they sit in. In public XML doc / JSDoc a concrete illustration is welcome where it reads as one possible implementation ("for example, an editor that emits several groups may…"), never as the definition of the behaviour.
3. Repository Structure
Umbraco-CMS/
├── src/ # 21 production projects
│ ├── Umbraco.Core/ # Domain contracts (interfaces only)
│ │ └── CLAUDE.md # ⭐ Core architecture guide
│ ├── Umbraco.Infrastructure/ # Service implementations
│ ├── Umbraco.Web.Common/ # Web utilities
│ ├── Umbraco.Web.UI/ # Main web application
│ ├── Umbraco.Cms.Api.Management/ # Management API
│ ├── Umbraco.Cms.Api.Delivery/ # Delivery API (headless)
│ ├── Umbraco.Cms.Api.Common/ # Shared API infrastructure
│ │ └── CLAUDE.md # ⭐ API patterns guide
│ ├── Umbraco.PublishedCache.HybridCache/ # Content caching
│ ├── Umbraco.Examine.Lucene/ # Search indexing
│ ├── Umbraco.Cms.Persistence.EFCore/ # EF Core data access
│ ├── Umbraco.Cms.Persistence.EFCore.Sqlite/
│ ├── Umbraco.Cms.Persistence.EFCore.SqlServer/
│ ├── Umbraco.Cms.Persistence.Sqlite/ # Legacy SQLite
│ ├── Umbraco.Cms.Persistence.SqlServer/ # Legacy SQL Server
│ ├── Umbraco.Cms.Imaging.ImageSharp/ # Image processing v1
│ ├── Umbraco.Cms.Imaging.ImageSharp2/ # Image processing v2
│ ├── Umbraco.Cms.StaticAssets/ # Embedded assets
│ ├── Umbraco.Cms.DevelopmentMode.Backoffice/
│ ├── Umbraco.Cms.Targets/ # NuGet targets
│ └── Umbraco.Cms/ # Meta-package
│
├── tests/ # 6 test projects
│ ├── Umbraco.Tests.Common/
│ ├── Umbraco.Tests.UnitTests/
│ ├── Umbraco.Tests.Integration/
│ ├── Umbraco.Tests.Benchmarks/
│ ├── Umbraco.Tests.AcceptanceTest/
│ └── Umbraco.Tests.AcceptanceTest.UmbracoProject/
│
├── templates/ # Project templates
│ └── Umbraco.Templates/
│
├── tools/ # Build tools
│ └── Umbraco.JsonSchema/
│
├── umbraco.sln # Main solution file
├── Directory.Build.props # Shared build configuration
├── Directory.Packages.props # Centralized package versions
├── .editorconfig # Code style
└── .globalconfig # Roslyn analyzersArchitecture Layers
Dependency Flow (unidirectional, always flows inward):
Web.UI → Web.Common → Infrastructure → Core
↓
Api.Management → Api.Common → Infrastructure → Core
↓
Api.Delivery → Api.Common → Infrastructure → CoreKey Principle: Core has NO dependencies (pure contracts). Infrastructure implements Core. Web/APIs depend on Infrastructure.
Project Dependencies
Core Layer:
Umbraco.Core→ No dependencies (only Microsoft.Extensions.*)
Infrastructure Layer:
Umbraco.Infrastructure→Umbraco.CoreUmbraco.PublishedCache.*→Umbraco.InfrastructureUmbraco.Examine.Lucene→Umbraco.InfrastructureUmbraco.Cms.Persistence.*→Umbraco.Infrastructure
Web Layer:
Umbraco.Web.Common→Umbraco.Infrastructure+ caching + searchUmbraco.Web.UI→Umbraco.Web.Common+ all features
API Layer:
Umbraco.Cms.Api.Common→Umbraco.Web.CommonUmbraco.Cms.Api.Management→Umbraco.Cms.Api.CommonUmbraco.Cms.Api.Delivery→Umbraco.Cms.Api.Common
4. Teamwork & Collaboration
Branching Strategy
- Main branch:
main(protected) - Branch naming convention:
v<version>/<type>/<description>
Format: v{major-version}/{type}/{kebab-case-description}
Version: Read from version.json in the repository root. Use the major version number (e.g., v17 for version 17.x.x).
Types:
| Type | Use Case |
|---|---|
feature |
New feature being introduced to the product |
bugfix |
Fix to an existing issue with the product |
qa |
Adding or updating unit, integration, or end-to-end tests |
improvement |
Update to something that already exists but isn't broken (UI finessing, refactoring) |
task |
Update that doesn't directly impact product behavior (dependency updates, build pipeline) |
Description: A short, kebab-case description (a few words). This should be prefixed with the GitHub issue number if the update is related to resolving a tracked issue.
Examples:
v17/bugfix/12345-correct-display-of-pending-migrations
v17/feature/add-webhook-support
v17/improvement/optimize-content-cache
v17/qa/add-media-service-tests
v17/task/update-ef-core-dependencySee .github/CONTRIBUTING.md for full guidelines.
Pull Request Process
- PR Template:
.github/pull_request_template.md - Required CI Checks:
- All tests pass
- Code formatting (dotnet format)
- No build warnings
- Merge Strategy: Squash and merge (via GitHub UI)
- Reviews: Required from code owners
PR Naming Convention
Use the format: Area: Description (closes #IssueID)
Examples:
| Area | Description | Issue |
|---|---|---|
| Relations: | Move persistence of relations from repository into notification handlers | (closes #00000) |
| Management API: | Correct the population of the parent for sibling items when retrieved under a folder | |
| Docs: | Updated contributing guidelines to welcome contributions on bugfixes |
Area: The feature or aspect affected (e.g., UFM, TipTap, Docs, Segmentation, Migrations). Helps readers quickly understand what is being changed.
Description Best Practices:
- Include the area of change (Relations, Management API, etc.)
- Describe the change and its impact
- Be specific, not vague (describe "a golden retriever" not just "a dog")
Issue Linking: Add (closes #IssueID) to the title for readability, AND include a closing keyword on its own line in the PR body (e.g., Fixes #IssueID) so GitHub actually auto-links and auto-closes the issue on merge. GitHub only parses closing keywords (closes, fixes, resolves) from the PR body or commit messages — the title suffix is cosmetic and does not trigger auto-close on its own.
Commit Messages
Follow Conventional Commits format:
<type>(<scope>): <description>
Types: feat, fix, docs, style, refactor, test, chore
Scope: project name (core, web, api, etc.)
Examples:
feat(core): add IContentService.GetByIds method
fix(api): resolve null reference in schema handler
docs(web): update routing documentationCode Owners
Project ownership is distributed across teams. Check individual project directories for ownership.
5. Architecture Patterns
Core Architectural Decisions
Layered Architecture with Dependency Inversion
- Core defines contracts (interfaces)
- Infrastructure implements contracts that need Infrastructure-owned machinery
- Web/APIs consume implementations via DI
Where service implementations live: Services whose dependencies are satisfiable from Core interfaces alone (repositories, scope, config, other Core services) live in
Umbraco.Core/Services/— this covers the majority of domain services (MemberService,ContentService,MediaService,ContentTypeService,EntityService,AuditService,ExternalMemberService, etc.). Service implementations only live inUmbraco.Infrastructure/Services/Implement/when they genuinely need Infrastructure concerns — Examine indexes (ContentSearchService,MediaSearchService,IndexedEntitySearchService), log files (LogViewerRepository), packaging internals (PackagingService), webhook firing (WebhookFiringService), distributed-job coordination (DistributedJobService). When adding a new service, default to Core and only move to Infrastructure if a concrete dependency forces it.Interface-First Design
- All services defined as interfaces in Core
- Enables testing, polymorphism, extensibility
Notification Pattern (not C# events)
- See
/src/Umbraco.Core/CLAUDE.md→ "2. Notification System (Event Handling)"
- See
Composer Pattern (DI registration)
- See
/src/Umbraco.Core/CLAUDE.md→ "3. Composer Pattern (DI Registration)"
- See
Scoping Pattern (Unit of Work)
- See
/src/Umbraco.Core/CLAUDE.md→ "5. Scoping Pattern (Unit of Work)"
- See
Attempt Pattern (operation results)
Attemptinstead of exceptions- Strongly-typed operation status enums
Key Design Patterns Used
- Repository Pattern - Data access abstraction
- Unit of Work - Scoping for transactions
- Builder Pattern -
ProblemDetailsBuilderfor API errors - Strategy Pattern - OpenAPI handlers (schema ID, operation ID)
- Options Pattern - All configuration via
IOptions<T> - Factory Pattern - Content type factories
- Mediator Pattern - Notification aggregator
6. Avoiding Breaking Changes
No binary breaking changes are allowed within a major version. Three patterns are used:
6.1 Obsolete Constructor + StaticServiceProvider
When a public class needs new dependencies, obsolete the existing constructor and add a new one. The old constructor delegates to the new one, resolving missing deps via StaticServiceProvider.
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 19.")]
public MyService(IDependencyA depA)
: this(
depA,
StaticServiceProvider.Instance.GetRequiredService<IDependencyB>())
{
}
public MyService(IDependencyA depA, IDependencyB depB)
{
_depA = depA;
_depB = depB;
}Examples:
ContentCollectionPresentationFactory- addedFlagProviderCollectionCacheInstructionService- addedILastSyncedManager,IRepositoryCacheVersionServiceDocumentPresentationFactory- addedFlagProviderCollection
Rules:
- Old constructor marked
[Obsolete("... Scheduled for removal in Umbraco {current-major+2}.")] - Old constructor calls new constructor via
: this(...) - Uses
StaticServiceProvider.Instance.GetRequiredService<T>()for new params only - DI registration must use the NEW constructor (old is for external consumers only)
6.2 Obsolete Method + New Overload
When a public method signature needs to change, add the new method/overload and obsolete the old. The obsolete method should call the new one with suitable defaults.
[Obsolete("Use the overload taking all parameters. Scheduled for removal in Umbraco 19.")]
public void DoThing(string name)
=> DoThing(name, extraParam: null);
public void DoThing(string name, string? extraParam)
{
// Real implementation here
}Rules:
- Old method marked
[Obsolete]with removal schedule - DRY: old method calls new method, providing defaults for new parameters
- All internal callers must be updated to use the new method
- No callers should remain on the obsolete method within the codebase
6.3 Default Interface Implementation
When adding methods to a public interface, provide a default implementation so existing external implementations don't break.
public interface IMyService
{
// Existing method
void ExistingMethod();
// New method with default implementation
void NewMethod(string param)
=> ExistingMethod(); // delegate to existing if possible
}Strategies for the default (in order of preference):
- Use existing interface methods to satisfy the contract (even if not optimal)
- Return a sensible default like empty collection, null, etc.
- Throw
NotImplementedExceptionif no reasonable default exists
Example: IContentService.SaveBlueprint - new overload with IContent? createdFromContent has a default impl that calls the old method (ignoring the new param).
Example: IDocumentPresentationFactory.CreateCulturePublishScheduleModels - full default implementation with logic, uses StaticServiceProvider for dependency resolution within the interface.
Rules:
- Add
// TODO (V{next-major}): Remove the default implementation when {obsolete method} is removed.comment - Default impl should be functionally correct even if not optimal
- If using
StaticServiceProviderin a default impl, note this is temporary
6.4 General Rules
- Removal policy: Obsoleted members must remain for at least one full major version before removal. If obsoleted in version N, the earliest removal is version N+2. For example, something obsoleted in v17 is scheduled for removal in v19 (giving the whole of v18 as a deprecation period).
- All
[Obsolete]attributes must include "Scheduled for removal in Umbraco {current+2}" - Read
version.jsonto determine the current major version - Suppress
CS0618warnings where obsolete members must call each other:csharp#pragma warning disable CS0618 // Type or member is obsolete => OldMethod(param); #pragma warning restore CS0618 // Type or member is obsolete - Update ALL internal callers to use the new API - no internal code should use obsolete members
7. Project-Specific Notes
Centralized Package Management
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
<!-- Individual projects reference WITHOUT version -->
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
<!-- Versions defined in Directory.Packages.props -->
<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).editorconfig- Code style rules.globalconfig- Roslyn analyzer rules
SBOM Generation & License Policy (Azure Pipelines)
build/azure-pipelines.yml generates a CycloneDX SBOM per project (Backend/NuGet, Login UI, Backoffice, E2E), each in its own job in the Dependency_Track stage, via umbraco-sbom (the Umbraco.Internal.Sbom .NET 10 global tool), not plain dotnet CycloneDX / @cyclonedx/cyclonedx-npm. It adds license-policy enforcement over CycloneDX. Repo (private): https://github.com/umbraco/Umbraco.Internal.Sbom — README has the full flag/exit-code/policy reference.
When editing those steps:
- Output filenames and artifacts (
bom-dotnet.xml,bom-login.xml,bom-backoffice.xml,bom-e2e.xml) are mapped to Dependency-Track projects in the stage'sbuild/templates/dependency-track.ymlparameters — keep them in step. - Usage:
umbraco-sbom --output-file <full-path> [policy flags]. Only--output-fileplus policy flags (--allow-package,--allow-commercial,--allow-copyleft,--fail-on-unknown,--policy-allow-file) exist. - Commercial packages need
--allow-package, else exit 50. Backend usesnuget:SixLabors.*(covers bothSixLabors.ImageSharpand.ImageSharp.Web). isLatest: DT allows one latest version per project name, and projects areUmbraco-CMS(childrenUmbraco-CMS-Backendetc.) versioned by major. Only a stable public release ofmain's major (read frommain'sversion.jsonat upload time) is marked latest; older-major releases upload withfalse.- Exit 10 = success-with-warnings (unresolved licenses; SBOM still written). Each step tolerates it:
if ($LASTEXITCODE -ne 0 -and $LASTEXITCODE -ne 10) { exit $LASTEXITCODE }; exit 0. - Every BOM job needs a
UseDotNet@2(useGlobalJson: true) step before the tool install (the .NET 10 tool needs the SDK, including in the otherwise Node-only frontend jobs). - Backend must restore before scanning:
umbraco-sbomneeds every project in the solution restored, including the two the solution restore skips (Umbraco.Tests.AcceptanceTest.UmbracoProject,Umbraco.JsonSchema).
Persistence Layer - NPoco and EF Core
The repository contains BOTH (actively supported):
- Current: NPoco-based persistence (
Umbraco.Cms.Persistence.Sqlite,Umbraco.Cms.Persistence.SqlServer) - widely used and fully supported - Future: EF Core-based persistence (
Umbraco.Cms.Persistence.EFCore.*) - migration in progress
Note: The codebase is actively migrating to EF Core, but NPoco remains the primary persistence layer and is not deprecated. Both are fully supported.
Authentication: OpenIddict
All APIs use OpenIddict (OAuth 2.0/OpenID Connect):
- Reference tokens (not JWT) for better security
- Secure cookie-based token storage (v17+) - tokens stored in HTTP-only cookies with
__Host-prefix - Tokens are redacted from client-side responses and passed via secure cookies only (
[redacted]placeholder) - ASP.NET Core Data Protection for token encryption
- Configured in
Umbraco.Cms.Api.Common - API requests must include credentials (
credentials: includefor fetch)
Load Balancing Requirement: All servers must share the same Data Protection key ring.
Frontend auth pitfalls — see src/Umbraco.Web.UI.Client/docs/edge-cases.md (Auth & Cross-tab section) and docs/security.md. Key points:
- Never call
validateToken()per API request — it revokes the previous reference token (ID2019 errors) window.openeris set for ANYwindow.open()target, not only OAuth popups — scope guards to the pathname too- BroadcastChannel does not deliver messages to the sender's own tab
Content Caching Strategy
HybridCache (Umbraco.PublishedCache.HybridCache):
- In-memory cache + distributed cache support
- Published content only (not draft)
- Invalidated via notifications and cache refreshers
API Versioning
APIs use Asp.Versioning.Mvc:
- Management API:
/umbraco/management/api/v{version}/* - Delivery API:
/umbraco/delivery/api/v{version}/* - OpenAPI docs:
/umbraco/openapi/management.json,/umbraco/openapi/delivery.json - Swagger UI:
/umbraco/openapi/
Updating `OpenApi.json` (Management API)
When a PR changes Management API controllers or models, the OpenApi.json file in the Management API project must be updated, along with the generated backoffice client that is derived from it.
With the Umbraco instance running locally (in a non-Production environment — Swagger isn't mapped in Production):
npm --prefix src/Umbraco.Web.UI.Client run generate:openapi
npm --prefix src/Umbraco.Web.UI.Client run generate:server-apiThe first fetches the document from /umbraco/swagger/management/swagger.json byte-for-byte into src/Umbraco.Cms.Api.Management/OpenApi.json; the second regenerates the hey-api client from that committed file. Both results must be committed together.
They are two separate commands on purpose. The client generator only ever reads the committed schema — never a running site — so the schema and the client it produced always land in the same commit.
The /umb-update-openapi skill wraps this: it starts and stops a backend for you if one isn't already running, and explains the diff. Reach for it when you want the whole round trip handled.
Important: The fetch is byte-for-byte on purpose, so the endpoint stays the source of truth. Don't reformat the result — commit only the substantive changes, not IDE-applied formatting (whitespace, reordering, etc.). Extraneous formatting diffs make PRs harder to review and merge-ups more error-prone.
Backoffice npm Package
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
- Circular Dependencies: Avoided via
Lazy<T>or event notifications - Multi-Server: Requires shared Data Protection key ring and synchronized clocks (NTP)
- Database Support: SQL Server, SQLite
8. CI/CD — Claude AI Assistant
Two GitHub Actions workflows powered by anthropics/claude-code-action@v1. Advisory only — does not block merging.
Workflows
| File | Trigger | Purpose |
|---|---|---|
claude-review.yml |
pull_request: [opened, ready_for_review] |
Auto-review every non-draft PR using the umb-review skill |
claude.yml |
@claude comments, issue assign/label |
Interactive assistant for PRs and issues |
Auto-Review (`claude-review.yml`)
Runs the full .claude/skills/umb-review/SKILL.md procedure on every newly opened or un-drafted PR. Produces inline comments per finding and one summary comment with a verdict. Skips draft PRs. No turn limit.
Interactive (`claude.yml`)
Responds to @claude mentions on PRs and issues. The trigger phrase is stripped before Claude sees the message, so:
@claude review→ light review usinggh pr diff(not the umb-review skill)@claude fix ...→ implements a fix on a new branch@claude help→ answers questions about the codebase@claude label→ applies labels@claude(empty) → defaults toreviewon PRs,helpon issues
Also triggers on issue assignment to claude or adding the claude label. Gated: only runs when @claude appears in the comment/issue body. Max 50 turns.
Allowed tools:
- Auto-review: inline comments,
gh auth status,gh pr view/diff/comment/edit, andgit diff/log/show/fetch. - Interactive: inline comments,
git,npmanddotnet, file edits inside the checkout (--permission-mode acceptEdits), and theghcommands for PRs, issues and runs that the requests need (gh pr view/diff/checks/comment/edit/create,gh issue view/comment/edit,gh search issues/prs,gh run view/list). Node and .NET are set up from.nvmrcandglobal.json.
Labels
Both workflows apply labels based on content:
On PRs (based on changed files):
| Label | Condition |
|---|---|
area/frontend |
Files under src/Umbraco.Web.UI.Client/ |
area/backend |
.cs files outside the frontend client |
area/test |
Only test files changed |
category/api |
Management or Delivery API files |
category/breaking |
Breaking changes detected |
category/localization |
Localization/language files |
category/test-automation |
Only test files changed |
category/refactor |
Pure refactoring, no new features |
category/performance |
Performance-related changes |
category/ux |
User-facing changes |
category/ui |
UI layer changes |
On Issues (based on content): same area/* and category/* labels, plus affected/v14 through affected/v17 and affected/backoffice.
Labels are only added, never removed. Claude applies only labels it is confident about.
Key Implementation Notes
- Checkout required — the action internally runs
git fetch origin mainfor trusted file restoration. Withoutactions/checkout, it fails withfatal: not a git repository. id-token: writepermission — required for OIDC token exchange with the Claude GitHub App.- Workflow files must match
main— the action skips, with a warning, when the workflow file it runs from differs from the version on the default branch. A PR that changesclaude-review.ymlcan't review itself, and reviews on other branches skip until the change has been merged up tomain. - Mode decides the default tools —
track_progress: trueruns the auto-review in tag mode, where the action adds read tools, file edits inside the checkout and its own commit tools. With apromptand no progress tracking, the interactive and issue-deduplication workflows run in agent mode, where only--allowedToolsapplies: read-only tools always work, but file edits need--permission-mode acceptEdits. - Trigger phrase stripping — the action strips
@claudefrom comments before passing to Claude. Prompts must reference commands without the prefix (e.g.,reviewnot@claude review). - PR number injection — the interactive workflow injects the PR/issue number into the prompt via
${{ github.event.issue.number }}since Claude can't discover it fromgh pr viewwhen checked out onmain.
9. Code Comment Policy
Default to no comment. Applies to all code in this repository — C#, TypeScript, Razor, build scripts. Identifiers, small functions, and recognized idioms (a guard clause, a null check, an early return) carry their own meaning, even when some other layer has a reason to trigger them redundantly — that reason belongs to that layer, not to a comment here. Write one only for what this code genuinely can't say on its own: a non-obvious why, an invariant the types don't enforce, or an edge case it deliberately handles. Public members get XML doc / JSDoc, kept concise.
When a comment is justified, pitch it at the altitude of the code it sits in — the vocabulary of the layer it lives in, not the incident that prompted it. §2 shows what that looks like in practice.
Comments aren't a changelog. An issue link (#21996) earns its place inline only while it's explaining a live why — a guard, a workaround, a regression test. It does not belong there as provenance (// Fix for X, // Used by Y, // See PR #1234); which task or PR produced a change lives in the commit message and PR description, where it stays accurate.
TODOs
The one exception to "default to no comment" — deleted once the TODO is done, so it can't rot. Keep them short and trackable, anchored to an author or a version trigger: // TODO (V19): remove once obsolete overload is gone, // TODO: pagination [NL].
10. Testing Practices
A test for shared code asserts the general rule from §2, not the scenario that reported it, and is named for the rule.
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.
11. Verification Discipline
- Fresh build before trusting a green. Never treat
--no-buildor cached/incremental output as proof a change compiles or passes — a stale run can mask a compile error. Rebuild before reporting build or test state. (Integration tests have a related false-green trap — seetests/Umbraco.Tests.Integration/CLAUDE.md.) - Grep the branch you think you're on. A search only supports a claim against the branch actually checked out, so confirm HEAD is where you expect before drawing a conclusion from a grep. Easy to get wrong whenever the tree moves under you — reviewing a PR head, switching worktrees, or mid merge-up/rebase.
- Remove unused usings — touched files only. Before handing back C# changes, remove unused
usingdirectives (IDE0005) from the.csfiles the change adds or modifies, and never sweep other files (unrelated churn causes merge-up conflicts). Run it per affected project, not the solution, which is slow to load:dotnet format style .csproj --diagnostics IDE0005 --severity info --include <files>, then rebuild (a using may only be needed under an#ifsymbol).
Quick Reference
Essential Commands
# Build solution
dotnet build
# Run all tests
dotnet test
# Run specific test category
dotnet test --filter "Category=Integration"
# Format code
dotnet format
# Pack all projects
dotnet pack -c ReleaseIntegration Test Database Configuration
Integration tests are configured in tests/Umbraco.Tests.Integration/appsettings.Tests.json.
The Tests:Database:DatabaseType setting controls which database is used:
"SQLite"(default) - No external dependencies"LocalDb"- Uses SQL Server LocalDB, required for SQL Server-specific tests (e.g., page-level locking,sys.dm_tran_locks)
SQL Server-specific tests use BaseTestDatabase.IsSqlite() to skip when running on SQLite.
Key Projects
| Project | Type | Description |
|---|---|---|
| Umbraco.Core | Library | Interface contracts and domain models |
| Umbraco.Infrastructure | Library | Service implementations and data access |
| Umbraco.Web.UI | Application | Main web application (Razor/MVC) |
| Umbraco.Cms.Api.Management | Library | Management API (backoffice) |
| Umbraco.Cms.Api.Delivery | Library | Delivery API (headless CMS) |
| Umbraco.Cms.Api.Common | Library | Shared API infrastructure |
| Umbraco.PublishedCache.HybridCache | Library | Published content caching |
| Umbraco.Examine.Lucene | Library | Full-text search indexing |
Important Files
- Solution:
umbraco.sln - Build Config:
Directory.Build.props,Directory.Packages.props - Code Style:
.editorconfig,.globalconfig - Documentation:
/CLAUDE.md,/src/Umbraco.Core/CLAUDE.md,/src/Umbraco.Cms.Api.Common/CLAUDE.md
Project-Specific Documentation
For detailed information about individual projects, see their CLAUDE.md files:
- Core Architecture:
/src/Umbraco.Core/CLAUDE.md- Service contracts, notification patterns - API Infrastructure:
/src/Umbraco.Cms.Api.Common/CLAUDE.md- OpenAPI, authentication, serialization - Backoffice Frontend:
/src/Umbraco.Web.UI.Client/CLAUDE.md- Lit web components, extension system, auth client
Important: When working on backoffice client code (anything under src/Umbraco.Web.UI.Client/), read /src/Umbraco.Web.UI.Client/CLAUDE.md first. It contains action-specific checklists (deprecation, testing, security, etc.) that are not duplicated here.
Getting Help
- Official Docs: https://docs.umbraco.com/
- Contributing Guide:
.github/CONTRIBUTING.md - Issues: https://github.com/umbraco/Umbraco-CMS/issues
- Community: https://forum.umbraco.com/
- Releases: https://releases.umbraco.com/
This repository follows a layered architecture with strict dependency rules. The Core defines contracts, Infrastructure implements them, and Web/APIs consume them. Each layer can be understood independently, but dependencies always flow inward toward Core.
File: context7.json
{
"url": "https://context7.com/umbraco/umbraco-cms",
"public_key": "pk_GTIgsrGAQiHNxCirZBDIM"
}
File: global.json
{
"sdk": {
"version": "10.0.100",
"rollForward": "latestFeature",
"allowPrerelease": false
}
}
File: LICENSE.md
The MIT License (MIT)
Copyright (c) 2005-present Umbraco
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
File: MCP.md
MCP (Model Context Protocol) Setup
This repository includes configuration for Model Context Protocol (MCP) servers, enabling AI tooling integration for Umbraco CMS development workflows.
Overview
MCP allows AI assistants (like Claude) to interact with external tools and services. This repository configures two MCP servers:
| Server | Purpose | Package |
|---|---|---|
| umbraco-cms | Manage Umbraco content types, documents, and media | @umbraco-cms/mcp-dev@17 |
| playwright | Browser automation for testing and debugging | @playwright/mcp@latest |
Quick Start
1. Start Umbraco Locally
Ensure your local Umbraco instance is running at https://localhost:44339 (or update the URL in your .env.local).
2. Configure Environment Variables
Copy the example environment file and customize it:
cp .env.example .env.localEdit .env.local with your local settings:
UMBRACO_CLIENT_ID=umbraco-back-office-mcp
UMBRACO_CLIENT_SECRET=<your-client-secret>
UMBRACO_BASE_URL=https://localhost:44339
NODE_TLS_REJECT_UNAUTHORIZED=0
UMBRACO_INCLUDE_TOOL_COLLECTIONS=data-type,document-type,document,media-type,media3. Configure the OAuth Client in Umbraco
Create an OAuth client in your Umbraco instance with:
- Client ID:
umbraco-back-office-mcp - Client Secret: The value you set in
.env.local - Grant Type: Client Credentials
Environment Variables Reference
| Variable | Description | Example |
|---|---|---|
UMBRACO_CLIENT_ID |
OAuth client ID configured in Umbraco | umbraco-back-office-mcp |
UMBRACO_CLIENT_SECRET |
OAuth client secret (keep secure!) | your-secure-secret |
UMBRACO_BASE_URL |
URL of your local Umbraco instance | https://localhost:44339 |
NODE_TLS_REJECT_UNAUTHORIZED |
Set to 0 for self-signed certificates (local dev only) |
0 |
UMBRACO_INCLUDE_TOOL_COLLECTIONS |
Comma-separated list of tool collections to enable | data-type,document-type,document |
Tool Collections
The UMBRACO_INCLUDE_TOOL_COLLECTIONS variable controls which Umbraco MCP tools are available:
data-type- Manage data types (property editors)document-type- Manage document types (content types)document- Manage content/documentsmedia-type- Manage media typesmedia- Manage media items
Security Considerations
Warning: This configuration is for local development only.
Self-Signed Certificates
NODE_TLS_REJECT_UNAUTHORIZED=0 disables SSL certificate validation. This is necessary for self-signed certificates in local development but:
- Never use in production
- Affects all HTTPS connections made by Node.js processes
- Consider trusting your local development certificate instead
Client Secrets
- Never commit real secrets to source control
- The
.env.localfile is gitignored for this reason - Use strong, unique secrets even in development
- The example value
1234567890in.env.exampleis a placeholder only
File Structure
Umbraco-CMS/
├── .mcp.json # MCP server configuration
├── .env.example # Example environment variables (committed)
├── .env.local # Your local environment variables (gitignored)
├── .claude/
│ ├── settings.json # Shared Claude AI permissions (committed)
│ └── settings.local.json # Local Claude overrides (gitignored)
├── .gitignore # Ignores .env.local and settings.local.json
└── MCP.md # This documentation (you are here)Claude AI Permissions
The .claude/settings.json file configures which MCP tools Claude can use automatically without prompting. This is shared across the team for consistent developer experience.
Customizing Permissions Locally
Create .claude/settings.local.json to override permissions for your environment:
{
"permissions": {
"allow": [
"mcp__umbraco__get-all-document-types"
]
}
}Troubleshooting
"Connection refused" errors
- Ensure Umbraco is running at the configured
UMBRACO_BASE_URL - Check that the port matches your local setup
"Unauthorized" errors
- Verify the OAuth client is configured in Umbraco
- Check that
UMBRACO_CLIENT_IDandUMBRACO_CLIENT_SECRETmatch - Ensure the client has appropriate permissions
"Certificate" errors
- For local development, set
NODE_TLS_REJECT_UNAUTHORIZED=0in.env.local - Alternatively, trust your local development certificate
MCP server not starting
- Ensure Node.js is installed (v22+ recommended, matching .nvmrc)
- Run
npx @umbraco-cms/mcp-dev@17 --helpto verify the package works
Further Reading
File: NOTICES.txt
Third-Party Notices
This file contains notices and attributions for third-party software used in the Umbraco CMS project.
Third-party software may contain dependencies that are not explicitly listed here.
This notice is not a license and does not grant any rights to use the third-party software.
Umbraco CMS is licensed under the MIT License, which can be found in the LICENSE file.
@openid/AppAuth-JS: An OpenID Connect and OAuth 2.0 client library for JavaScript
URL: https://github.com/openid/AppAuth-JS
License: Apache License, Version 2.0
Copyright: 2017 Google Inc.
AutoFixture: Write maintainable unit tests, faster
URL: https://github.com/AutoFixture/AutoFixture
License: MIT License
Copyright: 2013 Mark Seemann
Asp.Versioning.Mvc: A library for ASP.NET Core versioning
URL: https://github.com/dotnet/aspnet-api-versioning
License: MIT License
Copyright: .NET Foundation and contributors
Babel: A JavaScript compiler
URL: https://babeljs.io/
License: MIT License
Copyright: 2014-present Sebastian McKenzie and other contributors
BenchmarkDotNet: Powerful .NET library for benchmarking
URL: https://github.com/dotnet/BenchmarkDotNet
License: MIT License
Copyright: .NET Foundation and Contributors
Bogus: A simple and sane data generator for populating objects that supports different locales.
URL: https://github.com/bchavez/Bogus
License: MIT License
Copyright: 2015 Brian Chavez
CommandLineParser: Terse syntax C# command line parser for .NET
URL: https://github.com/commandlineparser/commandline
License: MIT License
Copyright: 2005-2015 Giacomo Stelluti Scala & Contributors
cross-env: A CLI tool to set environment variables across platforms
URL: https://github.com/kentcdodds/cross-env
License: MIT License
Copyright: 2017 Kent C. Dodds
Dazinator.Extensions.FileProviders: A library for file provider extensions
URL: https://github.com/dazinator/Dazinator.Extensions.FileProviders
License: MIT License
Copyright: 2016 Darrell
DOMPurify: A DOM-only XSS sanitizer for HTML, MathML and SVG
URL: https://github.com/cure53/DOMPurify
License: Apache License, Version 2.0
Copyright: 2025 Dr.-Ing. Mario Heiderich, Cure53
Element Internals Polyfill: A polyfill for the Element Internals API
URL: https://github.com/calebdwilliams/element-internals-polyfill
License: MIT License
Copyright: 2021 Caleb Williams
Eslint: A tool for identifying and reporting on patterns in JavaScript
URL: https://eslint.org/
License: MIT License
Copyright: OpenJS Foundation and other contributors
Examine: A search and indexing library for .NET
URL: https://github.com/Shazwazza/Examine
License: Microsoft Public License (Ms-PL)
Copyright: 2023 Shannon Deminick
Globals: A library for managing global variables in JavaScript
URL: https://github.com/sindresorhus/globals
License: MIT License
Copyright: Sindre Sorhus
Html Agility Pack: An HTML parser for .NET
URL: https://html-agility-pack.net/
License: MIT License
Copyright: ZZZ Projects Inc.
ImageSharp: A cross-platform library for processing images in .NET
URL: https://github.com/SixLabors/ImageSharp
License: Apache License, Version 2.0 under the Six Labors Split License
Copyright: Six Labors
jsdiff: A JavaScript text differencing implementation
URL: https://github.com/kpdecker/jsdiff
License: BSD 3-Clause License
Copyright: 2009-2015 Kevin Decker [email protected]
JsonPatch.Net: A library for JSON Patch (RFC 6902) in .NET
URL: https://github.com/json-everything/json-everything
License: MIT License
Copyright: .NET Foundation and Contributors
K4os.Compression.LZ4: A fast LZ4 compression library for .NET
URL: https://github.com/MiloszKrajewski/K4os.Compression.LZ4
License: MIT License
Copyright: 2017 Milosz Krajewski
Lit: A simple library for building fast, lightweight web components
URL: https://lit.dev
License: BSD 3-Clause License
Copyright: 2020 Google LLC. All rights reserved.
Lucide: Beautiful & consistent icons for the web
URL: https://lucide.dev/
License: ISC License
Copyright: 2013-2022 Cole Bemis
Copyright: 2022 Lucide Contributors
Madge: A dependency graph generator for JavaScript
URL: https://github.com/pahen/madge
License: MIT License
Copyright: 2017 Patrik Henningsson
MailKit: A library for sending email in .NET
URL: https://github.com/jstedfast/MailKit
License: MIT License
Copyright: 2013-2024 .NET Foundation and Contributors
Markdig: A fast, powerful, CommonMark compliant, extensible Markdown processor for .NET
URL: https://github.com/xoofx/markdig
License: BSD-2-Clause license
Copyright: 2018+, Alexandre Mutel. All rights reserved.
Markdown: A library for parsing and compiling Markdown
URL: https://github.com/hey-red/Markdown
License: MIT License
Copyright: 2018 red
marked: A markdown parser and compiler
URL: https://marked.js.org/
License: MIT License
Copyright: 2011-2018, Christopher Jeffrey (https://github.com/chjj/)
Copyright: 2018+, MarkedJS (https://github.com/markedjs/)
Message Pack: The extremely fast MessagePack serializer for C#
URL: https://github.com/MessagePack-CSharp/MessagePack-CSharp
License: MIT License
Copyright: 2017 Yoshifumi Kawai and contributors
Miniprofiler: A mini profiler for .NET
URL: https://github.com/MiniProfiler/dotnet
License: MIT License
Copyright: .NET MiniProfiler Contributors
Monaco Editor: A browser-based code editor
URL: https://microsoft.github.io/monaco-editor/
License: MIT License
Copyright: 2016-present Microsoft Corporation
Moq: A mocking library for .NET
URL: https://github.com/moq/moq
License: BSD 3-Clause License
Copyright: 2007 Clarius Consulting, Manas Technology Solutions, InSTEDD, and Contributors.
Mock Service Worker (MSW): A library for mocking API requests in JavaScript
URL: https://mswjs.io/
License: MIT License
Copyright: 2018–present Artem Zakharchenko
NCrontab: A cron schedule parser for .NET
URL: https://github.com/atifaziz/NCrontab
License: Apache License, Version 2.0
Copyright: 2001 The OpenSymphony Group
Copyright: 2008 Atif Aziz
Nerdbank.GitVersioning: A library for versioning .NET projects
URL: https://github.com/dotnet/Nerdbank.GitVersioning
License: MIT License
Copyright: .NET Foundation and Contributors
NJsonSchema: A JSON schema validator for .NET
URL: https://github.com/RicoSuter/NJsonSchema
License: MIT License
Copyright: 2022 Rico Suter
NPoco: A micro ORM for .NET
URL: https://github.com/schotime/NPoco
License: Apache License, Version 2.0
Copyright: Schotime
NUnit: A unit testing framework for .NET
URL: https://github.com/nunit/nunit
License: MIT License
Copyright: Charlie Poole, Rob Prouse and Contributors
Open Web Components: A set of standards and libraries for building web components
URL: https://open-wc.org/
License: MIT License
Copyright: 2018 open-wc
Openapi-ts: The OpenAPI to TypeScript codegen
URL: https://github.com/hey-api/openapi-ts
License: MIT License
Copyright: Hey API
OpenIddict: A simple and flexible OpenID Connect server for ASP.NET Core
URL: https://github.com/openiddict/openiddict-core
License: Apache License, Version 2.0
Copyright: Kévin Chalet
Playwright: A Node.js library to automate browser testing
URL: https://playwright.dev/
License: Apache License, Version 2.0
Copyright: 2025 Microsoft Corporation
Playwright-msw: A library to wrap Mock Service Worker with Playwright
URL: https://github.com/valendres/playwright-msw
License: MIT License
Copyright: 2022 Peter Weller
Prettier: An opinionated code formatter
URL: https://prettier.io/
License: MIT License
Copyright: James Long and contributors
Remark-gfm: A GitHub Flavored Markdown plugin for Remark
URL: https://github.com/remarkjs/remark-gfm
License: MIT License
Copyright: Titus Wormer
rxjs: Reactive Extensions for JavaScript
URL: https://rxjs.dev/
License: Apache License, Version 2.0
Copyright: 2015-present Ben Lesh , Google, Inc., Netflix, Inc., Microsoft Corp., and contributors
Serilog: A diagnostic logging library for .NET
URL: https://github.com/serilog/serilog
License: Apache License, Version 2.0
Copyright: Serilog Contributors
Simple Icons: A set of SVG icons for popular brands
URL: https://simpleicons.org/
License: CC0 1.0 Universal License
Copyright: Simple Icons Contributors
Storybook: A UI component explorer for Web Components
URL: https://storybook.js.org/
License: MIT License
Copyright: 2024 Storybook
StyleCop.Analyzers: Analyzers for StyleCop
URL: https://github.com/DotNetAnalyzers/StyleCopAnalyzers
License: MIT License
Copyright: Tunnel Vision Laboratories, LLC
SVGO: A tool for optimizing SVG files
URL: https://svgo.dev/
License: MIT License
Copyright: Kir Belevich
Swashbuckle.AspNetCore: A library for generating Swagger documentation for ASP.NET Core APIs
URL: https://github.com/domaindrivendev/Swashbuckle.AspNetCore
License: MIT License
Copyright: 2016 Richard Morris
Tiny Glob: A tiny globbing library for Node.js
URL: https://github.com/terkelg/tiny-glob
License: MIT License
Copyright: 2018 Terkel
Tiptap: A renderless rich-text editor for the web
URL: https://tiptap.dev/
License: MIT License
Copyright: 2025 Tiptap GmbH
Tsc-alias: A TypeScript compiler plugin for aliasing module paths
URL: https://github.com/justkey007/tsc-alias
License: MIT License
Copyright: 2018 Justkey
Typedoc: A documentation generator for TypeScript projects
URL: https://typedoc.org/
License: Apache License, Version 2.0
Copyright: Gerrit Birkeland and Contributors
Typescript: A typed superset of JavaScript that compiles to plain JavaScript
URL: https://www.typescriptlang.org/
License: Apache License, Version 2.0
Copyright: 2012-present Microsoft Corporation
Typescript-eslint: A set of tools for linting TypeScript code
URL: https://github.com/typescript-eslint/typescript-eslint
License: MIT License
Copyright: 2019 typescript-eslint and other contributors
Typescript-json-schema: A library for generating JSON schema from TypeScript types
URL: https://github.com/YousefED/typescript-json-schema
License: BSD 3-Clause License
Copyright: 2016 typescript-json-schema contributors
Umbraco.Code: Provides code-level tools for Umbraco
URL: https://github.com/umbraco/Umbraco-Code
License: MIT License
Copyright: 2005-present Umbraco A/S
Umbraco.GitVersioning.Extensions: Utilities for Nerdbank.GitVersioning
URL: https://github.com/umbraco/Umbraco.GitVersioning.Extensions
License: MIT License
Copyright: 2005-present Umbraco A/S
Umbraco.JsonSchema.Extensions: Utilities for JSON schema generation
URL: https://github.com/umbraco/Umbraco.JsonSchema.Extensions
License: MIT License
Copyright: 2005-present Umbraco A/S
Umbraco UI Library: A set of UI components for building web applications
URL: https://uui.umbraco.com/
License: MIT License
Copyright: 2005-present Umbraco A/S
uuid: A library for generating unique identifiers
URL: https://github.com/uuidjs/uuid
License: MIT License
Copyright: 2010-2020 Robert Kieffer and other contributors
Vite: A fast build tool and development server for modern web projects
URL: https://vite.dev/
License: MIT License
Copyright: 2019-present VoidZero Inc. and Vite contributors
Vite-plugin-static-copy: A Vite plugin for copying static files
URL: https://github.com/sapphi-red/vite-plugin-static-copy
License: MIT License
Copyright: 2021 sapphi-red
Vite-tsconfig-paths: A Vite plugin for resolving TypeScript paths
URL: https://github.com/aleclarson/vite-tsconfig-paths
License: MIT License
Copyright: Alec Larson
Web Component Analyzer: A tool for analyzing web components
URL: https://github.com/runem/web-component-analyzer
License: MIT License
Copyright: 2019 Rune Mehlsen
File: .mcp.json
{
"mcpServers": {
"umbraco-cms": {
"command": "npx",
"args": ["@umbraco-cms/mcp-dev@18", "--env", ".env.local"]
},
"playwright": {
"command": "npx",
"args": [
"@playwright/mcp@latest"
]
}
}
}
File: src/Umbraco.Cms.Api.Common/CLAUDE.md
Umbraco.Cms.Api.Common
Shared infrastructure for Umbraco CMS REST APIs (Management and Delivery).
1. Architecture
Type: Class Library (NuGet Package)
Target Framework: .NET 10.0
Purpose: Common API infrastructure - OpenAPI/Swagger, JSON serialization, OpenIddict authentication, problem details
Key Technologies
- ASP.NET Core - Web framework
- 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
Dependencies
Umbraco.Core- Domain models and service contractsUmbraco.Web.Common- Web functionality
Project Structure (46 files)
Umbraco.Cms.Api.Common/
├── 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
│ ├── ConfigureUmbracoOpenApiOptionsBase.cs
│ └── ConfigureOpenIddict.cs
├── DependencyInjection/ # Service registration
│ ├── UmbracoBuilderApiExtensions.cs
│ └── UmbracoBuilderAuthExtensions.cs
├── Builders/ # RFC 7807 problem details
│ └── ProblemDetailsBuilder.cs
├── ViewModels/Pagination/ # Common DTOs
└── Security/ # Auth paths and handlersDesign Patterns
- Builder Pattern -
ProblemDetailsBuilderfor fluent error responses - Options Pattern - All configuration via
IConfigureOptions<T>
2. Commands
See "Quick Reference" section at bottom for common commands.
3. Key Patterns
Schema ID Generation (OpenApi/UmbracoSchemaIdGenerator.cs)
Static utility class that generates OpenAPI schema IDs following Umbraco's naming conventions:
// 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),
// we need to add a "Model" postfix to all models
name = $"{name}Model";
}
// 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)
// IMPORTANT: do NOT return an empty enumerable here. it will cause nullability to fail on reference
// properties, because "$ref" does not mix and match well with "nullable" in OpenAPI.
if (type.IsInterface is false)
{
return new[] { type };
}Why: Interfaces must return concrete types to avoid OpenAPI schema conflicts.
4. Testing
Location: No direct tests - tested via integration tests in consuming APIs
How to test changes:
# Run integration tests that exercise this library
dotnet test tests/Umbraco.Tests.Integration/
# Verify OpenAPI generation
# 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:
- OpenAPI document generation (schema IDs, operation IDs)
- Polymorphic JSON serialization/deserialization
- OpenIddict authentication flow
- Problem details formatting
5. OpenIddict Authentication
Key Configuration (DependencyInjection/UmbracoBuilderAuthExtensions.cs)
Reference Tokens over JWT (line 76-80):
// Enable reference tokens
// - see https://documentation.openiddict.com/configuration/token-storage.html
options
.UseReferenceAccessTokens()
.UseReferenceRefreshTokens();Why: More secure (revocable), better for load balancing, uses ASP.NET Core Data Protection.
Token Lifetime (line 88-91):
// Make the access token lifetime 25% of the refresh token lifetime
options.SetAccessTokenLifetime(new TimeSpan(timeOut.Ticks / 4));
options.SetRefreshTokenLifetime(timeOut);PKCE Required (line 59-63):
// Enable authorization code flow with PKCE
options
.AllowAuthorizationCodeFlow()
.RequireProofKeyForCodeExchange()
.AllowRefreshTokenFlow();Endpoints: Backoffice /umbraco/management/api/v1/security/*, Member /umbraco/member/api/v1/security/*
Secure Cookie-Based Token Storage (v17+)
Implementation (DependencyInjection/HideBackOfficeTokensHandler.cs):
Back-office tokens are hidden from client-side JavaScript via HTTP-only cookies:
private const string AccessTokenCookieKey = "__Host-umbAccessToken";
private const string RefreshTokenCookieKey = "__Host-umbRefreshToken";
// Tokens are encrypted via Data Protection and stored in cookies
SetCookie(httpContext, AccessTokenCookieKey, context.Response.AccessToken);
context.Response.AccessToken = "[redacted]"; // Client sees redacted valueKey Security Features (lines 143-165): HttpOnly, IsEssential, Path="/", Secure (HTTPS), __Host- prefix
Configuration: BackOfficeTokenCookieSettings.Enabled (default: true in v17+)
Implications: Client-side cannot access tokens; encrypted with Data Protection; load balancing needs shared key ring; API requests need credentials: include
6. Common Issues & Edge Cases
Polymorphic Deserialization Requires `$type`
Issue: Deserializing to an interface without $type discriminator fails.
Handled in (Json/NamedSystemTextJsonInputFormatter.cs:24-29):
catch (NotSupportedException exception)
{
// This happens when trying to deserialize to an interface, without sending the $type as part of the request
context.ModelState.TryAddModelException(string.Empty, new InputFormatterException(exception.Message, exception));
return await InputFormatterResult.FailureAsync();
}Solution: Clients must include $type property for interface types, or use concrete types.
Schema ID Collisions with TypeScript
Issue: Type names like Document clash with TypeScript built-ins.
Solution: UmbracoSchemaIdGenerator adds "Model" suffix to all schema names.
Generic Type Handling
Issue: PagedViewModel<T> needs flattened schema name.
Solution: UmbracoSchemaIdGenerator.Generate() flattens generic types:
PagedViewModel<RelationItemViewModel>becomesPagedRelationItemModel
7. Extending This Library
Adding Custom OpenAPI Transformers
OpenAPI transformers are scoped per-document. To customize a document, implement IOpenApiDocumentTransformer, IOpenApiOperationTransformer, or IOpenApiSchemaTransformer and register with your OpenAPI options.
For schema ID generation, use the static UmbracoSchemaIdGenerator.Generate(Type) method.
Customizing Problem Details
var problemDetails = new ProblemDetailsBuilder()
.WithTitle("Validation Failed")
.WithDetail("The request contains errors")
.WithType("ValidationError")
.WithOperationStatus(MyOperationStatus.ValidationFailed)
.WithRequestModelErrors(errors)
.Build();
return BadRequest(problemDetails);8. Project-Specific Notes
Per-Document Transformer Scoping
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
Why: Cache discovered subtypes (UmbracoJsonTypeInfoResolver.cs:14) to avoid expensive reflection calls
Known Limitations
Polymorphic Deserialization:
- Requires
$typediscriminator in JSON for interfaces - Only discovers types in Umbraco namespaces
- Not all .NET types are discoverable
- Requires
OpenAPI Schema Generation:
- Generic types are flattened (e.g.,
PagedViewModel<T>→PagedTModel) - Type names may need "Model" suffix to avoid clashes
- Generic types are flattened (e.g.,
OpenIddict Multi-Server:
- Requires shared Data Protection key ring
- All servers must have synchronized clocks (NTP)
- Reference tokens require database storage
External Dependencies
OpenIddict:
- OAuth 2.0 / OpenID Connect provider
- Version: See
Directory.Packages.props - Uses ASP.NET Core Data Protection for token encryption
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
ApiVersionattribute - API explorer integration for multi-version Swagger docs
Configuration
HTTPS: DisableTransportSecurityRequirement for local dev only (ConfigureOpenIddict.cs:14). Warning: Never disable in production.
Usage Pattern
Consuming APIs call builder.AddUmbracoOpenApi().AddUmbracoOpenIddict()
Quick Reference
Essential Commands
# Build project
dotnet build src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj
# Pack for NuGet
dotnet pack src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj -c Release
# Test via integration tests
dotnet test tests/Umbraco.Tests.Integration/
# Check packages
dotnet list src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj package --outdated
dotnet list src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj package --vulnerableKey Classes
| Class | Purpose | File |
|---|---|---|
ProblemDetailsBuilder |
Build RFC 7807 error responses | Builders/ProblemDetailsBuilder.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 |
PagedViewModel<T> |
Generic pagination model | ViewModels/Pagination/PagedViewModel.cs |
Important Files
Umbraco.Cms.Api.Common.csproj- Project dependenciesDependencyInjection/UmbracoBuilderApiExtensions.cs- OpenAPI registration (line 12-31)DependencyInjection/UmbracoBuilderAuthExtensions.cs- OpenIddict setup (line 20-183)Security/Paths.cs- API endpoint path constants
Getting Help
- Root documentation:
/CLAUDE.md- Repository overview - Core patterns:
/src/Umbraco.Core/CLAUDE.md- Core contracts and patterns - Official docs: https://docs.umbraco.com/
- OpenIddict docs: https://documentation.openiddict.com/
This library is the foundation for all Umbraco CMS REST APIs. Focus on OpenAPI customization, authentication configuration, and polymorphic serialization when working here.
File: src/Umbraco.Cms.Api.Delivery/CLAUDE.md
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/ # OpenAPI configuration
└── Filters/ # Action filters (access, validation)Design Patterns
- Strategy Pattern - Query handlers (
ISelectorHandler,IFilterHandler,ISortHandler) - Factory Pattern -
ApiContentQueryFactorybuilds Examine queries - Template Method -
ContentApiControllerBasefor shared controller logic - Options Pattern -
DeliveryApiSettingsfor 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:
// 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:
- Selectors (
fetchparameter):ancestors:id,children:id,descendants:id - Filters (
filter[]parameter):contentType:alias,name:value,createDate>2024-01-01 - Sorts (
sort[]parameter):name:asc,createDate:desc,level:asc
// 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:
// 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
dotnet test tests/Umbraco.Tests.Integration/ --filter "FullyQualifiedName~Delivery"Internals exposed to (csproj lines 28-36):
Umbraco.Tests.UnitTestsUmbraco.Tests.IntegrationDynamicProxyGenAssembly2(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
// 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:
- Public - No authentication required (if enabled)
- API Key - Via
Api-Keyheader - 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/authorizePOST /umbraco/delivery/api/v1/security/member/tokenGET /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:
// 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
// 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):
// 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)
V1 Removal Pending (2 locations):
DependencyInjection/UmbracoBuilderExtensions.cs:98- FIXME: remove matcher policyRouting/DeliveryApiItemsEndpointsMatcherPolicy.cs:11- FIXME: remove class
Obsolete Reference Warnings (csproj:9-13):
ASP0019- IHeaderDictionary.Append usageCS0618/CS0612- Obsolete member references
Empty Query Results
Query service returns empty results (not errors) for invalid options:
// 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:
// 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 all8. 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):
// 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:
// 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)
{
"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 GUIDGET /item/{path}- Single content by routeGET /items- Multiple by IDsGET /- Query with fetch/filter/sort
Media (/umbraco/delivery/api/v2/media):
GET /item/{id}- Single media by GUIDGET /item/{path}- Single media by pathGET /items- Multiple by IDsGET /- Query media
Security (/umbraco/delivery/api/v1/security/member):
GET /authorize- Start OAuth flowPOST /token- Exchange code for tokenGET /signout- Revoke session
Quick Reference
Essential Commands
# 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 --outdatedKey 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 dependenciesDependencyInjection/UmbracoBuilderExtensions.cs- DI registration (lines 33-141)Configuration/DeliveryApiConfiguration.cs- API constantsServices/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.
File: src/Umbraco.Cms.Api.Management/CLAUDE.md
Umbraco CMS - Management API
RESTful API for Umbraco backoffice operations. Manages content, media, users, and system configuration through OpenAPI-documented endpoints.
Project: Umbraco.Cms.Api.Management
Type: ASP.NET Core Web API Library
Files: 1,317 C# files across 54+ controller domains
1. Architecture
Target Framework
- .NET 10.0 (
net10.0) - C# 12 with nullable reference types enabled
- ASP.NET Core Web API
Application Type
REST API Library - Plugged into Umbraco.Web.UI, provides the Management API surface for backoffice operations.
Key Technologies
- Web Framework: ASP.NET Core MVC with
Asp.Versioning.Mvc(v1.0 and v1.1) - 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
- Serialization: System.Text.Json with custom converters
- Mapping: Manual presentation factories (no AutoMapper)
- Patching: Custom patch engine for PATCH operations (Umbraco.Cms.Api.Management.Patching)
- ⚠️ Legacy JsonPatch.Net support (IJsonPatchService) still available but obsolete - scheduled for removal in v19
- Real-time: SignalR hubs (
BackofficeHub,ServerEventHub) - DI: Microsoft.Extensions.DependencyInjection via
ManagementApiComposer
Project Structure
src/Umbraco.Cms.Api.Management/
├── Controllers/ # 54+ domain-specific controller folders
│ ├── Document/ # Document (content) CRUD + publish/unpublish
│ ├── Media/ # Media CRUD + upload
│ ├── Member/ # Member management
│ ├── User/ # User management
│ ├── DataType/ # Data type configuration
│ ├── DocumentType/ # Content type schemas
│ ├── Template/ # Razor template management
│ ├── Dictionary/ # Localization dictionary
│ ├── Language/ # Language/culture config
│ ├── Security/ # Auth, login, external logins
│ ├── Install/ # Installation wizard
│ ├── Upgrade/ # Upgrade operations
│ ├── LogViewer/ # Log browsing
│ ├── HealthCheck/ # Health check dashboard
│ ├── Webhook/ # Webhook management
│ └── [48 more domains...]
│
├── ViewModels/ # Request/response DTOs (one folder per domain)
├── Factories/ # Domain model → ViewModel converters
├── Services/ # Business logic (thin layer over Core.Services)
├── Mapping/ # ViewModel → domain model mappers
├── Security/ # Auth providers, sign-in manager, external logins
├── OpenApi/ # OpenAPI transformers (schema, operation, security)
├── Routing/ # Route configuration, SignalR hubs
├── DependencyInjection/ # Service registration (55+ files)
├── Middleware/ # Preview, server events
├── Configuration/ # IOptions configurators
├── Filters/ # Action filters
├── Serialization/ # JSON converters
└── OpenApi.json # Embedded OpenAPI spec (1.3MB)Dependencies
- Umbraco.Cms.Api.Common - Shared API infrastructure (base controllers, OpenAPI config)
- Umbraco.Infrastructure - Service implementations, data access
- Umbraco.PublishedCache.HybridCache - Published content queries
- Microsoft.AspNetCore.OpenApi - OpenAPI document generation
- Swashbuckle.AspNetCore.SwaggerUI - Swagger UI
Design Patterns
Controller-per-Operation - Each endpoint is a separate controller class
- Example:
CreateDocumentController,UpdateDocumentController,DeleteDocumentController - Enables fine-grained authorization and operation-specific logic
- Responsibilities: entrypoint/routing, authorization and mapping
- avoid: business logic directly in controllers (there are a few known violations)
- Example:
Presentation Factory Pattern - Factories convert domain models to ViewModels
- Example:
IDocumentEditingPresentationFactory(src/Umbraco.Cms.Api.Management/Factories/) - Separation: Controllers → Factories → ViewModels
- Example:
Attempt Pattern - Operations return
Attemptfor status-based error handling- Controllers map status enums to HTTP status codes via helper methods
Authorization Service Pattern - All authorization via
IAuthorizationService, not attributes- Checked in base controller methods (see
ManagementApiControllerBase)
- Checked in base controller methods (see
Options Pattern - All configuration via
IOptions<T>(security, routing, OpenAPI)SignalR Event Broadcasting - Real-time notifications via
BackofficeHubandServerEventHub
API Response & Endpoint Conventions
These are firm design rules for this API. Prefer an extra round-trip over bending them.
Create/update (and other mutation) endpoints must not return the changed entity.
APOST/PUT/PATCHthat mutates state returns only status (e.g.Ok(), orCreatedAtId
with the new id/Location) — never the updated entity in the body. Clients that need the
resulting representation issue a separateGET. This keeps mutation endpoints minimal and
avoids clients depending on server-computed fields sneaking back through a write.- Why it matters: server-computed state can differ from what the client optimistically
expects (e.g. enabling a never-logged-in user leaves themInactive, notActive).
The correct fix is a read-back, not returning state from the mutation. - Batch read-back: when a client just mutated N entities and needs their fresh state,
add/use a bulk retrieval endpoint rather than N singleGETs. Pattern:GET {resource}/batch?id=...&id=...returningBatchResponseModel
(seeBatchDataTypesController,BatchUsersController). The batch controller lives under
the resource's normal controller base so it carries the same authorization as the
single-itemGET(section access + per-resource checks).
- Why it matters: server-computed state can differ from what the client optimistically
Do not add data to item endpoints (
/item/{resource}).
Item endpoints (Item*Controller, used to hydrate pickers/lists) sit onManagementApiControllerBasewith onlyBackOfficeAccess— deliberately looser
authorization than the section-scoped, per-resourceGETs. Any authenticated backoffice
user can call them. Never widen an item response model with fields that require stricter
authorization to see (state, permissions, sensitive/computed data) — that leaks information
past the resource's real authorization boundary. Put such fields on the properly-authorized
detail or batch endpoint instead.Versioning an endpoint the backoffice calls: pin the operation ID with a route
Name.
Operation IDs are generated from the route byUmbracoOperationIdTransformer, and a non-default[MapToApiVersion]is appended to them — so bumping an endpoint to1.1would rename the
generated client method (putDocumentByIdValidate→putDocumentByIdValidate1_1) and churn every
call site in the backoffice. The backoffice always calls the latest version, so the version in the
method name carries no information.Give the latest version an explicit name matching what the unversioned route would have
produced:csharp[HttpPut("{id:guid}/validate", Name = "PutDocumentByIdValidate")] [MapToApiVersion("1.1")]Rules:
Only the latest version gets the unversioned name. Older versions keep their generated
(version-suffixed) IDs — except at the default version (1.0), which is never suffixed.
An older1.0action therefore generates the very name you are pinning, so it needs an
explicit versioned name of its own at the same time:csharp[HttpPut("{id:guid}/validate", Name = "PutDocumentByIdValidate1_0")] [MapToApiVersion("1.0")]Two versions must never carry the same ID — duplicate operation IDs break OpenAPI
generation, and ASP.NET Core will not catch the clash, since route names are only unique when
both are explicit.OpenApiDocumentTestsguardsOpenApi.jsonagainst it. When moving the
name to a new version, remove it from the old one, which deliberately renames the old
operation.Match the generated convention (
{HttpMethod}{Path}By{Param}...) rather than inventing a
name, or the API surface ends up a mix of styles.Update
OpenApi.jsonand regenerate the backoffice client afterwards.
2. Commands
Build & Run
# Build
dotnet build src/Umbraco.Cms.Api.Management
# Test (tests in ../../tests/Umbraco.Tests.Integration and Umbraco.Tests.UnitTests)
dotnet test --filter "FullyQualifiedName~Management"
# Pack (for NuGet distribution)
dotnet pack src/Umbraco.Cms.Api.Management -c ReleaseCode Quality
# Format code
dotnet format src/Umbraco.Cms.Api.Management
# Build with warnings (note: some warnings suppressed, see .csproj line 23)
dotnet build src/Umbraco.Cms.Api.Management /p:TreatWarningsAsErrors=trueOpenAPI Documentation
The project embeds a pre-generated OpenApi.json (1.3MB). To regenerate:
# Run Umbraco.Web.UI, access /umbraco/openapi/
# Download JSON from /umbraco/openapi/management.jsonPackage Management
# Add package (versions centralized in Directory.Packages.props)
dotnet add src/Umbraco.Cms.Api.Management package [PackageName]
# Check for vulnerable packages
dotnet list src/Umbraco.Cms.Api.Management package --vulnerableEnvironment Setup
- Prerequisites: .NET 10 SDK
- IDE: Visual Studio 2022 or Rider (with .editorconfig support)
- Configuration: Inherits from
Umbraco.Web.UIappsettings (no app settings in this library)
3. Style Guide
Project-Specific Patterns
Controller Naming (line examples from CreateDocumentController.cs:16):
[ApiVersion("1.0")]
public class CreateDocumentController : CreateDocumentControllerBase- Pattern:
{Verb}{Entity}Controller(e.g.,CreateDocumentController,UpdateMediaController) - Base class:
{Verb}{Entity}ControllerBasefor shared logic - Critical: One operation per controller (not one controller per resource)
Async Naming - All async methods use Async suffix consistently:
await _contentEditingService.CreateAsync(model, CurrentUserKey(_backOfficeSecurityAccessor));Factory Pattern Usage (line 44):
ContentCreateModel model = _documentEditingPresentationFactory.MapCreateModel(requestModel);- ViewModels → Domain:
Map{Operation}Model(requestModel) - Domain → ViewModels: Factory classes in
Factories/folder
Key Patterns from Codebase
ControllerBase Helper Methods (inherited from ManagementApiControllerBase in Api.Common):
CreatedAtId<TController>(expression, id)- Returns 201 with Location headerContentEditingOperationStatusResult(status)- Maps status enum to ProblemDetailsCurrentUserKey(accessor)- Gets current user from security context
Authorization Pattern (all controllers):
private readonly IAuthorizationService _authorizationService;
// Check permissions in action, not via [Authorize] attribute4. Test Bench
Test Location
- Unit Tests:
tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Management/ - Integration Tests:
tests/Umbraco.Tests.Integration/Umbraco.Cms.Api.Management/
Running Tests
# All Management API tests
dotnet test --filter "FullyQualifiedName~Management"
# Specific domain (e.g., Document controllers)
dotnet test --filter "FullyQualifiedName~Management.Controllers.Document"Testing Focus
- Controller logic - Request validation, authorization checks, status code mapping
- Factories - ViewModel ↔ Domain model conversion accuracy
- Authorization - Policy enforcement for each operation
- OpenAPI schema - Ensure OpenAPI document generation doesn't break
InternalsVisibleTo
Tests have access to internal types (see .csproj:44-52):
Umbraco.Tests.UnitTestsUmbraco.Tests.IntegrationDynamicProxyGenAssembly2(for Moq)
5. Error Handling
Operation Status Pattern
Controllers use Attempt with strongly-typed status enums:
Attempt<ContentCreateResult, ContentEditingOperationStatus> result =
await _contentEditingService.CreateAsync(model, userKey);
return result.Success
? CreatedAtId<ByKeyDocumentController>(controller => nameof(controller.ByKey), result.Result.Content!.Key)
: ContentEditingOperationStatusResult(result.Status); // Maps to ProblemDetailsStatus Enums (from Core):
ContentEditingOperationStatus- InvalidParent, NotFound, NotAllowed, etc.UserOperationStatus- UserNameIsNotEmail, DuplicateUserName, etc.- Each enum value maps to specific HTTP status + ProblemDetails type
ProblemDetails
All errors return RFC 7807 ProblemDetails via helper methods in base controllers:
- 400 Bad Request: Validation failures, invalid operations
- 403 Forbidden: Authorization failures
- 404 Not Found: Resource not found
- 409 Conflict: Duplicate operations
Critical Logging Points
- Authorization failures - Logged by AuthorizationService
- Service operation failures - Logged in Infrastructure layer services
- External login errors - BackOfficeSignInManager (Security/)
6. Clean Code
Key Design Decisions
Why Controller-per-Operation? (not RESTful resource-based controllers)
- Fine-grained authorization per operation
- Operation-specific request/response models
- Clearer OpenAPI documentation
- Example: 20+ controllers in
Controllers/Document/for different operations
Why Manual Factories instead of AutoMapper?
- Explicit control over mapping logic
- Easier debugging (no magic)
- Better performance (no reflection overhead)
- See
Factories/directory (92 factory classes)
Project-Specific Architectural Decisions
Embedded OpenAPI Spec (OpenApi.json - 1.3MB):
- Pre-generated, embedded as resource
- Served for client SDK generation
- Why? Deterministic output, faster startup (no runtime generation)
SignalR for Real-time:
BackofficeHub(/umbraco/backofficeHub) - Generic payload broadcastServerEventHub(/umbraco/serverEventHub) - Entity change events, filtered per userPreviewHub(/umbraco/PreviewHub) - Content refresh notifications for the preview window- Every hub class carries
[Authorize(Policy = AuthorizationPolicies.BackOfficeAccess)].MapHubcopies it onto the negotiate and connection endpoints, so an unattributed hub is reachable anonymously.PreviewRoutesTestsandBackOfficeAreaRoutesTestsassert this for every mapped hub endpoint.
Code Smells to Watch For
Large factory classes - Some factories have 1000+ lines (e.g.,
UserGroupPresentationFactory.cs)- Consider splitting by operation
Repeated authorization checks - Each controller duplicates auth logic
- Already abstracted to base classes, but still verbose
ViewModel explosion - 1000+ ViewModel classes across ViewModels/ folders
- Consider shared base models or composition
7. Security
Authentication & Authorization
Method: OpenIddict (OAuth 2.0) via Umbraco.Cms.Api.Common
- Reference tokens (not JWT) stored in database
- Token validation via OpenIddict middleware
- ASP.NET Core Data Protection for token encryption
Authorization:
- Basic authorization is done trough policies and the
AuthorizeAttribute
// Example from DocumentTreeControllerBase.cs, the user needs at least access to a section that uses trees
[Authorize(Policy = AuthorizationPolicies.SectionAccessForContentTree)]- Authorization that needs (parts of) the payload are done manually trough the IAuthorizationService
// Example from CreateDocumentController.cs:22
private readonly IAuthorizationService _authorizationService;
// Authorization checked in base controller methods, not attributes
protected async Task<IActionResult> HandleRequest(request, Func<Task<IActionResult>> handler)
{
var authResult = await _authorizationService.AuthorizeAsync(User, request, policy);
// ...
}Policies (defined in Security/Authorization/):
ContentPermissionHandler- Document/Media CRUD permissionsSectionAccessHandler- Backoffice section accessUserGroupPermissionHandler- Admin operations
Password Requirements (Security/ConfigureBackOfficeIdentityOptions.cs:18):
- See Identity options configuration
External Login Providers
Location: Security/BackOfficeExternalLoginProviders.cs
- Google, Microsoft, OpenID Connect providers
- Auto-linking with
ExternalSignInAutoLinkOptions - Critical: Validate external claims before auto-linking users
Input Validation
FluentValidation used throughout:
- Request models validated automatically via MVC integration
- Custom validators in each domain folder (e.g.,
ViewModels/Document/Validators/)
Parameter Validation:
// Controllers validate IDs, keys before service calls
if (requestModel.Parent == null)
return BadRequest(new ProblemDetailsBuilder()...);API Security
CORS - Configured in Umbraco.Web.UI (not this project)
HTTPS Enforcement - Configured in Umbraco.Web.UI
Request Size Limits - Configured for file uploads in Umbraco.Web.UI
Security Headers - Handled by Umbraco.Web.UI middleware
Secrets Management
No secrets in this project - Configuration injected from parent application (Umbraco.Web.UI)
Dependency Security
# Check vulnerable packages
dotnet list src/Umbraco.Cms.Api.Management package --vulnerableSecurity Anti-Patterns to Avoid
- Never bypass authorization checks - All operations must authorize
- Never trust client validation - Always validate on server
- Never expose stack traces - ProblemDetails abstracts errors
- Never log sensitive data - User passwords, tokens, API keys
8. Teamwork and Workflow
⚠️ SKIPPED - This is a sub-project. See root /CLAUDE.md for repository-wide teamwork protocols.
9. Edge Cases
Domain-Specific Edge Cases
Document Operations:
Publishing with descendants - Can timeout on large trees
- Use
PublishDocumentWithDescendantsControllerwith result polling - See
Controllers/Document/PublishDocumentWithDescendantsResultController.cs
- Use
Recycle bin operations - Items in recycle bin can't be published
- Check
IsTrashedbefore publish operations
- Check
Public access rules - Affects authorization and routing
- See
Controllers/Document/CreatePublicAccessDocumentController.cs
- See
Media Upload:
Large file uploads - Request size limits in parent app
- Controllers accept multipart/form-data
- Temporary files cleaned by background job
Media picker - Can reference deleted media
- Validation in
Factories/checks for orphaned references
- Validation in
User Management:
External logins - Auto-linking can create duplicate users if email mismatches
- See
Security/ExternalSignInAutoLinkOptions.cs
- See
User groups - Deleting user group doesn't delete users
- Users reassigned to default group
Webhooks:
- Webhook failures - Failed webhooks retry with exponential backoff
- See
Controllers/Webhook/for configuration
- See
Known Gotchas (from TODO comments)
StyleSheet/Script/PartialView Tree Controllers - All have identical TODO comment:
TODO: [NL] This must return path segments for a query to work
// src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/StylesheetTreeControllerBase.cs
// src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/ScriptTreeControllerBase.cs
// src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/PartialViewTreeControllerBase.csBackOfficeController - External login TODO:
// src/Umbraco.Cms.Api.Management/Controllers/Security/BackOfficeController.cs
// TODO: Handle external logins properly10. Agentic Workflow
When to Add a New Endpoint
Decision Points:
- Does the operation fit an existing controller domain? (Document, Media, User, etc.)
- Is this a new CRUD operation or a custom action?
- Does this require new authorization policies?
Workflow:
Create Controller in appropriate
Controllers/{Domain}/folder- Follow naming:
{Verb}{Entity}Controller - Inherit from domain-specific base controller or
ManagementApiControllerBase
- Follow naming:
Define ViewModels in
ViewModels/{Domain}/- Request model (e.g.,
CreateDocumentRequestModel) - Response model (if not reusing existing)
- Request model (e.g.,
Create or Update Factory in
Factories/- Map request ViewModel → domain model
- Map domain result → response ViewModel
Authorization - Check required permissions in controller action
- Use
IAuthorizationService.AuthorizeAsync()
- Use
Service Layer - Call Core services (from
Umbraco.Core.Services)- Handle
Attempt<>results - Map status to HTTP status codes
- Handle
OpenAPI Annotations:
[ApiVersion("1.0")][MapToApiVersion("1.0")][ProducesResponseType(...)]for all status codes
Testing:
- Unit test controller logic
- Integration test end-to-end flow
Update OpenApi.json (if needed for client generation)
Quality Gates Before PR
- All tests pass
- Code formatted (
dotnet format) - No new warnings (check suppressed warnings list in .csproj:23)
- OpenAPI schema valid (check at
/umbraco/openapi/) - Authorization tested (unit + integration tests)
Common Pitfalls
- Forgetting authorization checks - Every operation must authorize
- Inconsistent status code mapping - Use base controller helpers
- Large factory classes - Split by operation if >500 lines
- Missing ProducesResponseType - Breaks OpenAPI client generation
- Not handling Attempt failures - Always check
result.Success - Returning the entity from a mutation - Create/update endpoints return status only; the client does a follow-up
GET(or{resource}/batchfor many). See "API Response & Endpoint Conventions" in §1. - Widening item endpoints - Never add authorization-sensitive fields to
/item/{resource}responses; they have looser auth. See "API Response & Endpoint Conventions" in §1.
11. Project-Specific Notes
Key Design Decisions
Why 1,317 files for one API?
- Controller-per-operation pattern = many controllers
- 54 domains × average 10-20 operations per domain
- Tradeoff: Verbose but explicit, easier to navigate than megacontrollers
Why manual factories instead of AutoMapper?
- Performance: No reflection overhead
- Debuggability: Step through mapping logic
- Control: Complex mappings (e.g., security trimming) require custom logic
Why embedded OpenApi.json?
- Deterministic OpenAPI spec for client generation
- Faster startup (no runtime generation)
- Easier versioning (commit changes to spec)
- Added
OpenAPIContractTestto test that all operations are exported
External Integrations
None directly in this project. All integrations handled by:
- Umbraco.Infrastructure - External search, media, email providers
- Umbraco.Core - External data sources, webhooks
Known Limitations
API Versioning: Currently v1.0 and v1.1
- Future versions will require new controller classes or action methods
Batch Operations: Limited batch endpoint support
- Most operations are single-entity (create one document at a time)
Real-time Limits: SignalR hubs don't scale beyond single-server without Redis backplane
- Configure Redis for multi-server setups
File Upload Size: Controlled by parent app (Umbraco.Web.UI)
- This project doesn't set limits
Performance Considerations
Caching:
- Published content cached via
Umbraco.PublishedCache.HybridCache - Controllers query cache, not database (for published content)
Background Jobs:
- Long-running operations (publish with descendants, export) return job ID
- Poll result endpoint for completion
- See
Controllers/Document/PublishDocumentWithDescendantsResultController.cs
OpenAPI Generation:
- Pre-generated (OpenApi.json embedded)
- Runtime generation disabled for performance
Technical Debt (Top Issues from TODO comments)
Warnings Suppressed (Umbraco.Cms.Api.Management.csproj:9-22):
textTODO: Fix and remove overrides: - SA1117: params all on same line - SA1401: make fields private - SA1134: own line attributes - CS0108: hidden inherited member - CS0618/CS9042: update obsolete references - CS1998: remove async or make method synchronous - CS8524: switch statement exhaustiveness - IDE0060: removed unused parameter - SA1649: file name match type - CS0419: ambiguous reference - CS1573: param tag for all parameters - CS1574: unresolveable crefTree Controller Path Segments (multiple files):
- Stylesheet/Script/PartialView tree controllers need path segment support for queries
- Files:
Controllers/Stylesheet/Tree/StylesheetTreeControllerBase.cs - Files:
Controllers/Script/Tree/ScriptTreeControllerBase.cs - Files:
Controllers/PartialView/Tree/PartialViewTreeControllerBase.cs
External Login Handling (Controllers/Security/BackOfficeController.cs):
- TODO: Handle external logins properly
Large Factory Classes (Factories/UserGroupPresentationFactory.cs):
- Some factories exceed 1000 lines - consider splitting
ViewModel Explosion - 1000+ ViewModel classes
- Consider shared base models or composition to reduce duplication
Quick Reference
Essential Commands
# Build
dotnet build src/Umbraco.Cms.Api.Management
# Test Management API
dotnet test --filter "FullyQualifiedName~Management"
# Format code
dotnet format src/Umbraco.Cms.Api.Management
# Pack for NuGet
dotnet pack src/Umbraco.Cms.Api.Management -c ReleaseKey Projects
- Umbraco.Cms.Api.Management (this) - Management API controllers and models
- Umbraco.Cms.Api.Common - Shared API infrastructure, base controllers
- Umbraco.Infrastructure - Service implementations, data access
- Umbraco.Core - Domain models, service interfaces
Important Files
- Project file:
src/Umbraco.Cms.Api.Management/Umbraco.Cms.Api.Management.csproj - Composer:
src/Umbraco.Cms.Api.Management/ManagementApiComposer.cs(DI entry point) - Routing:
src/Umbraco.Cms.Api.Management/Routing/BackOfficeAreaRoutes.cs - OpenAPI Spec:
src/Umbraco.Cms.Api.Management/OpenApi.json(embedded, 1.3MB) - Base Controllers: See
Umbraco.Cms.Api.Commonproject
Configuration
No appsettings in this library - all configuration from parent app (Umbraco.Web.UI):
- OpenIddict settings
- CORS settings
- File upload limits
API Endpoints
Base path: /umbraco/management/api/v1/
Examples:
POST /umbraco/management/api/v1/document- Create documentGET /umbraco/management/api/v1/document/{id}- Get document by keyPUT /umbraco/management/api/v1/document/{id}- Update documentDELETE /umbraco/management/api/v1/document/{id}- Delete document
Full spec: See OpenApi.json or Swagger UI at /umbraco/openapi/
Getting Help
- Root Documentation:
/CLAUDE.md(repository overview) - API Common Docs:
../Umbraco.Cms.Api.Common/CLAUDE.md(shared API patterns) - Core Docs:
../Umbraco.Core/CLAUDE.md(domain architecture) - Official Docs: https://docs.umbraco.com/umbraco-cms/reference/management-api
File: src/Umbraco.Cms.DevelopmentMode.Backoffice/CLAUDE.md
Umbraco.Cms.DevelopmentMode.Backoffice
Development-time library enabling InMemoryAuto ModelsBuilder mode with runtime Razor view compilation. Allows content type changes to instantly regenerate strongly-typed models without application restart.
1. Architecture
Type: Class Library (NuGet Package)
Target Framework: .NET 10.0
Purpose: Enable hot-reload of ModelsBuilder models during development
Key Technologies
- Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation - Runtime Razor view compilation
- Microsoft.CodeAnalysis.CSharp (Roslyn) - Runtime C# compilation
- AssemblyLoadContext - Collectible assembly loading/unloading
Dependencies
Umbraco.Web.Common- Umbraco web infrastructure
Project Structure (18 source files)
Umbraco.Cms.DevelopmentMode.Backoffice/
├── DependencyInjection/
│ ├── BackofficeDevelopmentComposer.cs # Auto-registration via IComposer
│ └── UmbracoBuilderExtensions.cs # DI setup (CRITICAL: contains 80-line design overview)
└── InMemoryAuto/
├── InMemoryModelFactory.cs # Core factory - generates/loads models (876 lines)
├── InMemoryAssemblyLoadContextManager.cs # Manages collectible AssemblyLoadContext
├── RoslynCompiler.cs # Compiles generated C# to DLL
├── CollectibleRuntimeViewCompiler.cs # Custom IViewCompiler for Razor (489 lines)
├── UmbracoViewCompilerProvider.cs # Provides CollectibleRuntimeViewCompiler
├── RuntimeCompilationCacheBuster.cs # Clears Razor caches on model rebuild
├── UmbracoRazorReferenceManager.cs # Manages Roslyn MetadataReferences
├── CompilationOptionsProvider.cs # Mirrors host app compilation settings
├── UmbracoAssemblyLoadContext.cs # Collectible AssemblyLoadContext wrapper
├── ChecksumValidator.cs # Validates precompiled view checksums
├── CompilationExceptionFactory.cs # Creates detailed compilation errors
├── UmbracoCompilationException.cs # Custom exception with CompilationFailures
├── ModelsBuilderAssemblyAttribute.cs # Marks InMemory assemblies
├── ModelsBuilderBindingErrorHandler.cs # Handles model binding version mismatches
├── InMemoryModelsBuilderModeValidator.cs # Validates runtime mode configuration
└── ModelsModeConstants.cs # "InMemoryAuto" constantDesign Patterns
- Clone-and-Own Pattern - ASP.NET Core's RuntimeViewCompiler and related classes are cloned because internal APIs can't be extended
- Collectible AssemblyLoadContext - Enables assembly unloading for hot-reload
- FileSystemWatcher - Monitors
~/umbraco/Data/TEMP/InMemoryAuto/for changes - Lazy Initialization - References resolved on first use, not startup
2. Key Patterns
Why Clone-and-Own (CRITICAL CONTEXT)
The 80-line comment in DependencyInjection/UmbracoBuilderExtensions.cs:13-80 explains why this project exists. Key points:
- Problem: ASP.NET Core's
RuntimeViewCompilerloads assemblies into the defaultAssemblyLoadContext, which can't reference collectible contexts (breaking change in .NET 7) - Failed Solutions:
- Reflection to clear caches (unstable, internal APIs)
- Service wrapping via DI (still loads into wrong context)
- Solution: Clone
RuntimeViewCompiler,RazorReferenceManager,ChecksumValidator, and related classes, modifying them to:- Load compiled views into the same collectible
AssemblyLoadContextas models - Explicitly add InMemoryAuto models assembly reference during compilation
- Load compiled views into the same collectible
InMemoryModelFactory Lifecycle
Content Type Changed → Reset() called → _pendingRebuild = true
↓
Next View Request → EnsureModels() → GetModelsAssembly(forceRebuild: true)
↓
GenerateModelsCode() → RoslynCompiler.CompileToFile()
↓
ReloadAssembly() → InMemoryAssemblyLoadContextManager.RenewAssemblyLoadContext()
↓
RuntimeCompilationCacheBuster.BustCache() → Views recompile with new modelsAssembly Caching Strategy
Models are cached to disk for faster boot (InMemoryModelFactory.cs:400-585):
Hash Check:
TypeModelHasher.Hash(typeModels)creates hash of content type definitionsCache Files (in
~/umbraco/Data/TEMP/InMemoryAuto/):models.hash- Current content type hashmodels.generated.cs- Generated sourceall.generated.cs- Combined source with assembly attributesall.dll.path- Path to compiled DLLCompiled/generated.cs{hash}.dll- Compiled assembly
Boot Sequence:
- Check if
models.hashmatches current hash - If match, load existing DLL
- If mismatch or missing, recompile
- Check if
Collectible AssemblyLoadContext
// InMemoryAssemblyLoadContextManager.cs:29-37
internal void RenewAssemblyLoadContext()
{
_currentAssemblyLoadContext?.Unload(); // Unload previous
_currentAssemblyLoadContext = new UmbracoAssemblyLoadContext(); // Create new collectible
}Key constraint: No external references to the AssemblyLoadContext allowed - prevents unloading.
3. Error Handling
Compilation Failures
CompilationExceptionFactory.cs creates UmbracoCompilationException with:
- Source file content
- Generated code
- Diagnostic messages with line/column positions
Errors logged in CollectibleRuntimeViewCompiler.cs:383-396 and InMemoryModelFactory.cs:341-354.
Model Binding Errors
ModelsBuilderBindingErrorHandler.cs handles version mismatches between:
- View's model type (from compiled view assembly)
- Content's model type (from InMemoryAuto assembly)
Reports detailed error messages including assembly versions.
4. Security
Runtime Mode Validation: InMemoryModelsBuilderModeValidator.cs prevents InMemoryAuto mode outside BackofficeDevelopment runtime mode.
Temp File Location: Models compiled to ~/umbraco/Data/TEMP/InMemoryAuto/ - ensure this directory isn't web-accessible.
5. Edge Cases & Known Issues
Technical Debt (TODOs)
Circular Reference -
InMemoryModelFactory.cs:46:csharpprivate readonly Lazy<UmbracoServices> _umbracoServices; // TODO: this is because of circular refs :(DynamicMethod Performance -
InMemoryModelFactory.cs:698-701:csharp// TODO: use Core's ReflectionUtilities.EmitCtor !! // Yes .. DynamicMethod is uber slow
Race Conditions
InMemoryModelFactory.cs:800-809 - FileSystemWatcher can cause race conditions on slow cloud filesystems. Own file changes are always ignored.
Unused Assembly Cleanup
InMemoryModelFactory.cs:587-612 - TryDeleteUnusedAssemblies may fail with UnauthorizedAccessException if files are locked. Cleanup retried on next rebuild.
Reflection for Cache Clearing
RuntimeCompilationCacheBuster.cs:50-51 uses reflection to call internal RazorViewEngine.ClearCache():
Action<RazorViewEngine>? clearCacheMethod = ReflectionUtilities.EmitMethod<Action<RazorViewEngine>>("ClearCache");6. Configuration
Enable InMemoryAuto mode (appsettings.json):
{
"Umbraco": {
"CMS": {
"Runtime": {
"Mode": "BackofficeDevelopment"
},
"ModelsBuilder": {
"ModelsMode": "InMemoryAuto"
}
}
}
}Requirements:
RuntimeModemust beBackofficeDevelopmentModelsModemust beInMemoryAuto
Quick Reference
Essential Commands
# Build
dotnet build src/Umbraco.Cms.DevelopmentMode.Backoffice/Umbraco.Cms.DevelopmentMode.Backoffice.csproj
# Pack for NuGet
dotnet pack src/Umbraco.Cms.DevelopmentMode.Backoffice/Umbraco.Cms.DevelopmentMode.Backoffice.csproj -c Release
# Run integration tests
dotnet test tests/Umbraco.Tests.Integration/ --filter "FullyQualifiedName~ModelsBuilder"Note: This library has no direct tests - tested via integration tests in Umbraco.Tests.Integration.
Focus areas when modifying:
- Assembly loading/unloading cycles
- Razor view compilation with models
- Cache invalidation timing
- Model binding error scenarios
Essential Classes
| Class | Purpose | File |
|---|---|---|
InMemoryModelFactory |
Core model generation/loading | InMemoryAuto/InMemoryModelFactory.cs |
CollectibleRuntimeViewCompiler |
Custom Razor compiler | InMemoryAuto/CollectibleRuntimeViewCompiler.cs |
InMemoryAssemblyLoadContextManager |
Collectible assembly management | InMemoryAuto/InMemoryAssemblyLoadContextManager.cs |
RuntimeCompilationCacheBuster |
Cache invalidation | InMemoryAuto/RuntimeCompilationCacheBuster.cs |
UmbracoBuilderExtensions |
DI setup + design rationale | DependencyInjection/UmbracoBuilderExtensions.cs |
Important Files
- Design Overview:
DependencyInjection/UmbracoBuilderExtensions.cs:13-80- READ THIS FIRST - Project File:
Umbraco.Cms.DevelopmentMode.Backoffice.csproj - Model Factory:
InMemoryAuto/InMemoryModelFactory.cs- Most complex class
Cloned Microsoft Code
These files are clones of ASP.NET Core internals - check upstream for updates:
ChecksumValidator.cs- https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Razor.RuntimeCompilation/src/ChecksumValidator.csUmbracoRazorReferenceManager.cs- https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Razor.RuntimeCompilation/src/RazorReferenceManager.csCompilationOptionsProvider.cs- Partial clone of CSharpCompilerCompilationExceptionFactory.cs- Partial clone of CompilationFailedExceptionFactory
Getting Help
- Root Documentation:
/CLAUDE.md - Core Patterns:
/src/Umbraco.Core/CLAUDE.md - Official Docs: https://docs.umbraco.com/umbraco-cms/reference/configuration/modelsbuildersettings
This library enables hot-reload of content models during development. The core complexity is working around ASP.NET Core's internal Razor compilation APIs. Always read the design overview in UmbracoBuilderExtensions.cs:13-80 before making changes.
File: src/Umbraco.Cms.Imaging.ImageSharp/CLAUDE.md
Umbraco.Cms.Imaging.ImageSharp
Image processing library using ImageSharp 3.x and ImageSharp.Web for on-the-fly image manipulation, resizing, cropping, and caching.
1. Architecture
Type: Class Library (NuGet Package)
Target Framework: .NET 10.0
Purpose: Provide image manipulation via query string parameters
Key Technologies
- SixLabors.ImageSharp - Image processing library
- SixLabors.ImageSharp.Web - ASP.NET Core middleware for query string-based image manipulation
Both packages validate a licence key at build time; the key and its renewal details are in SIXLABORS-LICENSE.md.
Dependencies
Umbraco.Web.Common- Web infrastructure
Project Structure (13 source files)
Umbraco.Cms.Imaging.ImageSharp/
├── ImageSharpComposer.cs # Auto-registration via IComposer
├── UmbracoBuilderExtensions.cs # DI setup and middleware configuration
├── ConfigureImageSharpMiddlewareOptions.cs # Middleware options (caching, HMAC, size limits, decode throttle)
├── ConfigurePhysicalFileSystemCacheOptions.cs # File cache location
├── ImageProcessingThrottleMiddleware.cs # Bounds concurrent processing; owns the slot lifetime
├── ImageProcessingSlot.cs # One request's claim on the concurrency limit
├── ImageProcessingMemory.cs # Derives and applies the memory bounds (shared with ImageSharp2)
├── ImageProcessingThrottle.cs # How long a request waits, and how it is turned away
├── ImageProcessingUnavailableException.cs # Raised from the decode hook when the wait is given up on
├── ImageProcessors/
│ └── CropWebProcessor.cs # Custom crop processor with EXIF awareness
└── Media/
├── ImageSharpDimensionExtractor.cs # Extract image dimensions (EXIF-aware)
├── ImageSharpImageUrlGenerator.cs # Generate query string URLs for processing
└── ImageSharpImageUrlTokenGenerator.cs # Re-sign image URLs after an HMAC key rotationRelationship to ImageSharp2
Two imaging packages exist:
- Umbraco.Cms.Imaging.ImageSharp (this package) - Uses ImageSharp 3.x (default)
- Umbraco.Cms.Imaging.ImageSharp2 - Uses ImageSharp 2.x for backwards compatibility
Key difference: ImageSharp 3.x WebP encoder defaults to Lossless (10x larger files), so this package explicitly sets WebpFileFormatType.Lossy at ConfigureImageSharpMiddlewareOptions.cs:108-115.
2. Key Patterns
Query String Image Processing
Images are processed via URL query parameters handled by ImageSharp.Web middleware:
| Parameter | Purpose | Example |
|---|---|---|
width / height |
Resize dimensions | ?width=800&height=600 |
mode |
Crop mode (pad, crop, stretch, etc.) | ?mode=crop |
anchor |
Crop anchor position | ?anchor=center |
cc |
Crop coordinates (custom) | ?cc=0.1,0.1,0.1,0.1 |
rxy |
Focal point | ?rxy=0.5,0.3 |
format |
Output format | ?format=webp |
quality |
Compression quality | ?quality=80 |
Pipeline Integration
The wiring lives in UmbracoBuilderExtensions.cs, in the pre-pipeline of the ImageSharpComposer
pipeline filter, and its order matters:
- The memory bounds are applied first, while the pipeline is being built, so nothing can decode
before they are in force. ImageProcessingThrottleMiddlewareis registered ahead of the imaging middleware, because it
must own the request before the decode hook fires.UseImageSharp()runs before static files, or the query string is ignored and the source is
served as-is.
EXIF Orientation Handling
Both ImageSharpDimensionExtractor and CropWebProcessor account for EXIF rotation:
// ImageSharpDimensionExtractor.cs:42-44 - Swap width/height for rotated images
size = IsExifOrientationRotated(imageInfo)
? new Size(imageInfo.Height, imageInfo.Width)
: new Size(imageInfo.Width, imageInfo.Height);// CropWebProcessor.cs:64-65 - Transform crop coordinates for EXIF orientation
Vector2 xy1 = ExifOrientationUtilities.Transform(new Vector2(left, top), Vector2.Zero, Vector2.One, orientation);HMAC Request Authorization
When HMACSecretKey is configured, URLs are signed to prevent abuse (ImageSharpImageUrlGenerator.cs:121-131):
if (_options.HMACSecretKey.Length != 0 && _requestAuthorizationUtilities is not null)
{
var token = _requestAuthorizationUtilities.ComputeHMAC(uri, CommandHandling.Sanitize);
queryString.Add(RequestAuthorizationUtilities.TokenCommand, token);
}3. Configuration
ImagingSettings (appsettings.json)
{
"Umbraco": {
"CMS": {
"Imaging": {
"HMACSecretKey": "",
"Cache": {
"BrowserMaxAge": "7.00:00:00",
"CacheMaxAge": "365.00:00:00",
"CacheHashLength": 12,
"CacheFolder": "~/umbraco/Data/TEMP/MediaCache",
"CacheFolderDepth": 8
},
"Resize": {
"MaxWidth": 5000,
"MaxHeight": 5000
},
"Memory": {
"Enabled": false,
"MaximumPoolSizeMegabytes": 0,
"MaximumConcurrentProcessing": 0,
"MaximumDecodedImageMegabytes": 0
}
}
}
}
}Memory Settings (`ImagingMemorySettings`)
Each numeric value defaults to 0, meaning "derive from the memory available to the process"
(GC.GetGCMemoryInfo().TotalAvailableMemoryBytes, which honours a container limit).
| Setting | Purpose | Default |
|---|---|---|
Enabled |
Master switch for imaging memory management. When false, none of the three bounds is applied and ImageSharp's own memory behaviour is left untouched. |
false in v17/v18, true from v19 |
MaximumPoolSizeMegabytes |
Caps the unmanaged buffer pool ImageSharp retains between requests | available / 32, clamped to 16-64 MB |
MaximumConcurrentProcessing |
Caps how many images are processed at once | (available / 2) / 64 MB, capped at processor count |
MaximumDecodedImageMegabytes |
Caps any single buffer allocated while decoding an image | available / 4, clamped to 256-1024 MB |
ImagingMemorySettings in Umbraco.Core carries only these four values. What each is derived as,
and whether it applies at all, lives in ImageProcessingMemory in this project — the policy only
means anything against ImageSharp's own defaults, which Core knows nothing about.
The feature is off by default in v17/v18 and on from v19 (Enabled), so a minor upgrade
cannot change how an existing site allocates image memory — a site opts in until the major ships it
on. When enabled, each bound is still conditional — it engages only where it earns its keep —
and setting a value explicitly overrides that test, so an operator who names a number gets it.
| Bound | Engages when | Test |
|---|---|---|
| Pool cap | Under 4 GB is available to the process | ImageProcessingMemory.RequiresPoolSizeLimit |
| Single image | Under 4 GB is available to the process | ImageProcessingMemory.RequiresAllocationLimit |
| Concurrency | Under 4 GB is available to the process | ImageProcessingMemory.RequiresConcurrencyLimit |
All three engage on the same flat memory threshold. The pool cap is about retention, and
ImageSharp's default there is an eighth of available memory on any 64-bit host — GetDefaultMaxPoolSizeBytes
returns total / 8 when Environment.Is64BitProcess, and a flat 128 MB otherwise. It is never
disproportionate; it is a problem only in absolute terms, where that eighth competes with the memory
the rest of the site needs. Hence a flat memory threshold rather than a ratio.
Concurrency once engaged on a different test — derived < ProcessorCount, on the reasoning that
decoding is CPU bound so the cores already cap it. But requests in flight are thread-pool bound, not
core bound, so a low-core host still holds many concurrent decodes, and that test left the gate off
on exactly the low-core, memory-limited instances that most needed it — a 1-core B1 App Service SKU
being the case in point (#23556). It now
engages on the shared memory threshold, and ProcessorCount survives only as an upper clamp on
the resolved value (ResolveMaximumConcurrentProcessing), so on a host with cores to spare the bound
simply lands there.
The single-image ceiling exists because the concurrency bound assumes a cost per image
(EstimatedMegabytesPerImage, measured against a 12 megapixel JPEG). Peak is count x size, and
bounding only the count leaves the size trusted — a 100 megapixel source decodes to roughly 400 MB,
so even a derived limit of 3 would exhaust a 512 MB container. This bounds the other factor, and it
maps onto ImageSharp's AllocationLimitMegabytes, whose own default is a flat 1 GB on a 32-bit
process and 4 GB on a 64-bit one.
It applies per allocation, not per image: a decode makes several, so the total can still exceed
the ceiling. What it caps is the dominant one — the pixel buffer, roughly width x height x 3 for a
JPEG — which is enough to catch a source far larger than the host can serve. Over the limit,
ImageSharp throws InvalidMemoryOperationException wrapped in an InvalidImageContentException
blaming "possibly degenerate dimensions", so ImageProcessingThrottleMiddleware logs a warning
naming the setting before letting the failure through. Note this is unrelated to Resize.MaxWidth
and Resize.MaxHeight, which bound the output dimensions, not the source decode.
A request over the limit waits, then after ImageProcessingThrottle.WaitTimeout (30 seconds) is
turned away with 503 and a Retry-After, logged as a warning. It must not be let through
unthrottled on expiry instead — concurrent decodes are the thing being bounded, so that reinstates
the OOM under sustained load.
Enabled: false is the one-setting escape hatch that restores stock ImageSharp behaviour — and,
until v19, the default. Configure reports two independent facts about it, each once at startup:
- On a host a bound would have engaged on (under the 4 GB threshold), it says so at
Information, naming the available memory, the threshold and the switch
(Umbraco:CMS:Imaging:Memory:Enabled) — so an operator staring at an exit 137 on a memory-limited
box is pointed at the setting rather than left to find it. On a host with memory to spare it stays
quiet (Debug), so an unaffected site says nothing. - If any bound has been set explicitly while the switch is off, that is a contradiction in the
configuration rather than a property of the host, so it is reported at Information whatever
the host, naming the settings being discarded.
Each bound reports itself at startup — Information when it engages, naming the resolved value, and
Debug when it does not, so an unaffected site running at Information says nothing. Those lines are
the first thing to ask for when diagnosing either an exit 137 or an unexplained change in image
throughput.
Why these exist: a source image is decoded at full resolution before any processor runs, andImageSharpMiddleware only de-duplicates concurrent requests for the same URL. A page of distinct
thumbnails therefore decodes every source in parallel, so peak memory isconcurrent requests x decoded source size — measured at ~60 MB for one 300x300 thumbnail of a
4000x3000 JPEG. Unbounded, that exhausts a container limit and the process is killed (exit 137).
The concurrency cap is applied in two stages, and requests over it wait rather than being rejected.
ImageProcessingThrottleMiddleware, registered ahead ofUseImageSharp()in the pre-pipeline,
decides whether a request could decode: it needs a registered processor command and a format
that resolves to a configured image format, so an unrelated response like/export.csv?format=xlsxis left alone. For those that qualify it publishes anImageProcessingSlotonHttpContext.Itemsand releases it when the request ends, if nothing
else has by then.ConfigureImageSharpMiddlewareOptionswiresOnBeforeLoadAsync, which ImageSharp invokes on a
cache miss only, after the source is resolved and immediately before the decode. That is
where the wait happens, so a cache hit never queues behind a decode.
Splitting it this way keeps the wait precise while leaving a release somewhere it is guaranteed to
run. The place is normally given back from OnProcessedAsync, which ImageSharp invokes once the
decoded image has been disposed and before the result is written to the cache and sent — so the
cache write and the response, which hold only the encoded result, do not occupy a place. The
middleware's finally releases again when the request ends, which is a no-op after the first, and
covers a request that faulted or was turned away before reaching the hook.
ImageSharp 2.x differs on the way in, not the way out. ImageSharp.Web 2.0.2 has noOnBeforeLoadAsync (its earliest hook, OnParseCommandsAsync, runs before the cache check), soUmbraco.Cms.Imaging.ImageSharp2 takes the place in the middleware for anything its request filter
matches — cache hits included. It does have OnProcessedAsync, so the release is the same as above.
The two copies of ImageProcessingThrottleMiddleware are therefore not interchangeable; the v2
copy is the coarser fallback.
ImageSharp's own pool default is an eighth of available memory on a 64-bit process, released only on
a gen2 collection and then at most 50% per minute, which leaves a container sitting well above its
working set at rest. This memory is unmanaged, so no DOTNET_GC* setting governs it.
Security: Max Dimension Limits
ConfigureImageSharpMiddlewareOptions.cs enforces max dimensions on every request, regardless of whether HMAC is configured:
- Width/height requests exceeding
MaxWidth/MaxHeightare stripped from the query - This prevents DoS via excessive image generation
HMAC and the size limits do two separate jobs: HMAC controls who may request processing, while MaxWidth/MaxHeight control how large the output may be. The dimension ceiling therefore still applies to validly-signed requests — for legitimate (server-generated) URLs it is a no-op, and it remains a useful backstop if the HMAC secret key ever leaks.
Cache Busting
Query parameters rnd or v trigger immutable cache headers (ConfigureImageSharpMiddlewareOptions.cs:86-106):
- Disables
MustRevalidate - Adds
immutabledirective
4. Core Interfaces Implemented
| Interface | Implementation | Purpose |
|---|---|---|
IImageDimensionExtractor |
ImageSharpDimensionExtractor |
Extract width/height from streams |
IImageUrlGenerator |
ImageSharpImageUrlGenerator |
Generate manipulation URLs |
IImageWebProcessor |
CropWebProcessor |
Custom crop with cc parameter |
5. Edge Cases
WebP Encoding Change (ImageSharp 3.x)
ConfigureImageSharpMiddlewareOptions.cs:108-115 - ImageSharp 3.x defaults WebP to Lossless for PNGs, creating ~10x larger files. This is overridden:
options.Configuration.ImageFormatsManager.SetEncoder(
WebpFormat.Instance,
new WebpEncoder { FileFormat = WebpFileFormatType.Lossy });Crop Coordinates Format
CropWebProcessor.cs:50-56 - The cc parameter expects 4 values as distances from edges:
- Format:
left,top,right,bottom(0-1 range, percentages) - Right/bottom values are distance from those edges, not coordinates
- Zero values (
0,0,0,0) are ignored (no crop)
Supported File Types
Dynamically determined from ImageSharp configuration (ImageSharpDimensionExtractor.cs:24):
SupportedImageFileTypes = configuration.ImageFormats.SelectMany(f => f.FileExtensions).ToArray();Default includes: jpg, jpeg, png, gif, bmp, webp, tiff, etc.
Quick Reference
Essential Commands
# Build
dotnet build src/Umbraco.Cms.Imaging.ImageSharp/Umbraco.Cms.Imaging.ImageSharp.csproj
# Run tests
dotnet test tests/Umbraco.Tests.UnitTests/ --filter "FullyQualifiedName~ImageSharp"Key Files
| File | Purpose |
|---|---|
UmbracoBuilderExtensions.cs |
DI registration and pipeline setup |
ConfigureImageSharpMiddlewareOptions.cs |
Middleware config (caching, HMAC, size limits) |
CropWebProcessor.cs |
Custom cc crop parameter |
ImageSharpImageUrlGenerator.cs |
URL generation with HMAC signing |
URL Examples
# Basic resize
/media/image.jpg?width=800
# Crop to aspect ratio with focal point
/media/image.jpg?width=800&height=600&mode=crop&rxy=0.5,0.3
# Custom crop coordinates (10% from each edge)
/media/image.jpg?cc=0.1,0.1,0.1,0.1
# Format conversion with quality
/media/image.jpg?format=webp&quality=80
# Cache busted (immutable headers)
/media/image.jpg?width=800&v=abc123Getting Help
- Root Documentation:
/CLAUDE.md - ImageSharp Docs: https://docs.sixlabors.com/
- Umbraco Imaging: https://docs.umbraco.com/umbraco-cms/reference/configuration/imagingsettings
This library provides query string-based image processing. Key concerns are EXIF orientation handling, WebP encoding defaults, and HMAC security for public-facing sites.
File: src/Umbraco.Cms.Imaging.ImageSharp/SIXLABORS-LICENSE.md
Six Labors licence key
sixlabors.lic is the Six Labors licence key that the SixLabors.ImageSharp and SixLabors.ImageSharp.Web packages validate at build time. Release builds fail without a valid key; Debug builds only warn. The project points at the file through the SixLaborsLicenseFile property in Umbraco.Cms.Imaging.ImageSharp.csproj.
| License ID | ctm_01kwvyngdh9te11b5pqern2w9c |
| License Type | Community (assembly-scoped) |
| Assembly | Umbraco.Cms.Imaging.ImageSharp |
| Valid until | 10-01-2028 |
The key is assembly-scoped, so it is valid only for the assembly named above and may be committed to this public repository. Use the file exactly as issued; do not edit its contents. A new project that references a Six Labors package directly must have its assembly name added to the key when it is renewed.
Renewal
Renew in the final 90 days before expiry (from 12-10-2027) by resubmitting the form at https://licensing.sixlabors.com/ from the same email address used for the original application, selecting OpenSource and listing the exact assembly names. Replace sixlabors.lic with the new file and update the details above.
Background
See the Six Labors announcement, Security Support and License Enforcement.
File: src/Umbraco.Cms.Imaging.ImageSharp2/CLAUDE.md
Umbraco.Cms.Imaging.ImageSharp2
Image processing library using ImageSharp 2.x for backwards compatibility with existing deployments. Use this package only when migrating from older Umbraco versions that depend on ImageSharp 2.x behavior.
Namespace Note: Uses Umbraco.Cms.Imaging.ImageSharp (same as v3 package) for drop-in replacement - no code changes needed when switching.
1. Architecture
Type: Class Library (NuGet Package)
Target Framework: .NET 10.0
Purpose: ImageSharp 2.x compatibility layer
Package Versions (Pinned)
<!-- From csproj lines 7-8 -->
<PackageReference Include="SixLabors.ImageSharp" VersionOverride="[2.1.11, 3)" />
<PackageReference Include="SixLabors.ImageSharp.Web" VersionOverride="[2.0.2, 3)" />Version constraint [2.1.11, 3) means: minimum 2.1.11, below 3.0.
Project Structure (7 source files, plus 5 linked from the 3.x package)
Umbraco.Cms.Imaging.ImageSharp2/
├── ImageSharpComposer.cs # Auto-registration via IComposer
├── UmbracoBuilderExtensions.cs # DI setup, memory bounds and middleware configuration
├── ConfigureImageSharpMiddlewareOptions.cs # Middleware options (caching, size limits, decode slot release)
├── ImageProcessingThrottleMiddleware.cs # Bounds concurrent processing (2.x variant: waits before the pipeline)
├── ImageProcessors/
│ └── CropWebProcessor.cs # Custom crop processor with EXIF awareness
└── Media/
├── ImageSharpDimensionExtractor.cs # Extract image dimensions (EXIF-aware)
└── ImageSharpImageUrlGenerator.cs # Generate query string URLsFive more files are compiled in from the 3.x package as linked sources rather than copied, because
nothing in them touches an API that differs between the two majors. The linked-source group in the
csproj is the list. Edit them in the 3.x project; both packages pick up the change.
2. Key Differences from ImageSharp (3.x)
| Feature | ImageSharp2 (this) | ImageSharp (3.x) |
|---|---|---|
| Package version | 2.1.11 - 2.x | 3.x+ |
| HMAC signing | Not supported | Supported |
| WebP default | Lossy (native) | Lossless (overridden to Lossy) |
| Cache buster param | rnd only |
rnd or v |
| API differences | Image.Identify(config, stream) |
Image.Identify(options, stream) |
| Size property | image.Image.Size() method |
image.Image.Size property |
| Decode throttle | Waits before the pipeline, cache hits included | Waits in OnBeforeLoadAsync, cache misses only |
API Differences in Code
ImageSharpDimensionExtractor (Media/ImageSharpDimensionExtractor.cs:31):
// v2: Direct method call
IImageInfo imageInfo = Image.Identify(_configuration, stream);
// v3: Uses DecoderOptions
ImageInfo imageInfo = Image.Identify(options, stream);CropWebProcessor (ImageProcessors/CropWebProcessor.cs:67):
// v2: Size is a method
Size size = image.Image.Size();
// v3: Size is a property
Size size = image.Image.Size;Missing Features (vs ImageSharp 3.x)
- No HMAC request authorization -
HMACSecretKeysetting is ignored - No
vcache buster - Onlyrndparameter triggers immutable headers - No WebP encoder override - Uses default Lossy encoding (no configuration needed)
- No
OnBeforeLoadAsynchook - The decode throttle waits before the pipeline instead (see below)
Image Processing Memory
Both packages bound image processing memory the same way, from the linked ImageProcessingMemory:
a pool cap, a single-image ceiling and a concurrency limit, all off by default until v19 and all
governed by Umbraco:CMS:Imaging:Memory. The derivation, the engagement tests and the startup
logging are documented once, in the 3.x package's CLAUDE.md under "Memory Settings".
The one difference is where a gated request waits. ImageSharp.Web 2.0.2 has noOnBeforeLoadAsync, so this package's ImageProcessingThrottleMiddleware takes its place before
the downstream pipeline runs - a cache hit, or a missing source falling through to a 404, waits as
much as a decode does. The release is shared: OnProcessedAsync exists in 2.x, soConfigureImageSharpMiddlewareOptions gives the place back as soon as the decoded image is
disposed, with the end of the request as the backstop.
One consequence of having no decode hook: if ImageSharp.Web retries a request because a freshly
cached result could not be read back, the second decode runs after the place has been released and
nothing takes it again. The 3.x package re-enters its decode hook on a retry and re-acquires.
3. When to Use This Package
Use ImageSharp2 when:
- Migrating from Umbraco versions that used ImageSharp 2.x
- Third-party packages have hard dependency on ImageSharp 2.x
- Need exact byte-for-byte output compatibility with existing cached images
Use ImageSharp (3.x) when:
- New installations
- Need HMAC URL signing for security
- Want latest performance improvements
4. Configuration
Same as ImageSharp 3.x. See /src/Umbraco.Cms.Imaging.ImageSharp/CLAUDE.md → Section 3 for full configuration details.
Key difference: HMACSecretKey setting exists but is ignored in this package (no HMAC support in v2).
Quick Reference
Essential Commands
# Build
dotnet build src/Umbraco.Cms.Imaging.ImageSharp2/Umbraco.Cms.Imaging.ImageSharp2.csproj
# Run tests
dotnet test tests/Umbraco.Tests.UnitTests/ --filter "FullyQualifiedName~ImageSharp"Key Files
| File | Purpose |
|---|---|
Umbraco.Cms.Imaging.ImageSharp2.csproj |
Version constraints (lines 7-8) |
ConfigureImageSharpMiddlewareOptions.cs |
Size limit enforcement; releases the decode slot |
ImageProcessingThrottleMiddleware.cs |
Concurrency gate (waits before the pipeline) |
Media/ImageSharpImageUrlGenerator.cs |
URL generation (no HMAC) |
Switching Between Packages
To switch from ImageSharp2 to ImageSharp (3.x):
- Remove
Umbraco.Cms.Imaging.ImageSharp2package reference - Add
Umbraco.Cms.Imaging.ImageSharppackage reference - Clear media cache folder (
~/umbraco/Data/TEMP/MediaCache) - No code changes needed (same namespace)
Getting Help
- ImageSharp 3.x Documentation:
/src/Umbraco.Cms.Imaging.ImageSharp/CLAUDE.md - Root Documentation:
/CLAUDE.md - SixLabors ImageSharp 2.x Docs: https://docs.sixlabors.com/
This is a backwards-compatibility package. For new projects, use Umbraco.Cms.Imaging.ImageSharp (3.x) instead.
File: src/Umbraco.Cms.Persistence.EFCore/CLAUDE.md
Umbraco.Cms.Persistence.EFCore
Entity Framework Core persistence layer for Umbraco CMS. This project provides EF Core integration including scoping, distributed locking, and migration infrastructure.
Project Type: Class Library (NuGet package)
Target Framework: net10.0
Dependencies: Umbraco.Core, Umbraco.Infrastructure
1. Architecture
Project Purpose
This project bridges EF Core with Umbraco's existing persistence infrastructure. It provides:
- UmbracoDbContext - Base DbContext with automatic table prefix (
umbraco) and provider-based configuration - EF Core Scoping - Integration with Umbraco's Unit of Work pattern (scopes)
- Distributed Locking - SQL Server and SQLite locking mechanisms for EF Core contexts
- Migration Infrastructure - Provider-agnostic migration execution
Folder Structure
Umbraco.Cms.Persistence.EFCore/
├── Composition/
│ └── UmbracoEFCoreComposer.cs # DI registration, OpenIddict setup
├── Extensions/
│ ├── DbContextExtensions.cs # ExecuteScalarAsync, MigrateDatabaseAsync
│ └── UmbracoEFCoreServiceCollectionExtensions.cs # AddUmbracoDbContext<T>
├── Locking/
│ ├── SqlServerEFCoreDistributedLockingMechanism.cs
│ └── SqliteEFCoreDistributedLockingMechanism.cs
├── Migrations/
│ ├── IMigrationProvider.cs # Provider-specific migration execution
│ └── IMigrationProviderSetup.cs # DbContext options setup per provider
├── Scoping/
│ ├── AmbientEFCoreScopeStack.cs # AsyncLocal scope stack
│ ├── EFCoreScope.cs # Main scope implementation
│ ├── EFCoreDetachableScope.cs # Detachable scope for Deploy
│ ├── EFCoreScopeProvider.cs # Scope factory
│ ├── EFCoreScopeAccessor.cs # Ambient scope accessor
│ └── I*.cs # Interfaces
├── Constants-ProviderNames.cs # SQLite/SQLServer provider constants
├── EFCoreMigrationExecutor.cs # Migration orchestrator
├── StringExtensions.cs # Provider name comparison
└── UmbracoDbContext.cs # Base DbContextKey Design Decisions
- Generic DbContext Support - All scoping/locking is generic (``) allowing custom contexts
- Parallel NPoco Coexistence - Designed to work alongside existing NPoco persistence layer
- Provider Abstraction -
IMigrationProviderandIMigrationProviderSetupenable database-agnostic migrations - OpenIddict Integration - UmbracoDbContext automatically registers OpenIddict entity sets
Database Providers
Provider-specific implementations live in separate projects:
Umbraco.Cms.Persistence.EFCore.SqlServer- SQL Server provider and migrationsUmbraco.Cms.Persistence.EFCore.Sqlite- SQLite provider and migrations
Provider names (from Constants.ProviderNames):
Microsoft.Data.SqlClient- SQL ServerMicrosoft.Data.Sqlite- SQLite
2. Commands
For Git workflow and standard build commands, see repository root.
EF Core Migrations
Important: Run from repository root with valid connection string in src/Umbraco.Web.UI/appsettings.json.
# Add migration (SQL Server)
dotnet ef migrations add %MigrationName% -s src/Umbraco.Web.UI -p src/Umbraco.Cms.Persistence.EFCore.SqlServer -c UmbracoDbContext
# Add migration (SQLite)
dotnet ef migrations add %MigrationName% -s src/Umbraco.Web.UI -p src/Umbraco.Cms.Persistence.EFCore.Sqlite -c UmbracoDbContext
# Remove last migration (SQL Server)
dotnet ef migrations remove -s src/Umbraco.Web.UI -p src/Umbraco.Cms.Persistence.EFCore.SqlServer
# Remove last migration (SQLite)
dotnet ef migrations remove -s src/Umbraco.Web.UI -p src/Umbraco.Cms.Persistence.EFCore.Sqlite
# Generate migration script
dotnet ef migrations script -s src/Umbraco.Web.UI -p src/Umbraco.Cms.Persistence.EFCore.SqlServer3. Key Patterns
Adding a Custom DbContext
Use AddUmbracoDbContext<T> to register with full scope integration:
services.AddUmbracoDbContext<MyCustomDbContext>((provider, options, connectionString, providerName) =>
{
options.UseOpenIddict(); // Optional
});Registers: IDbContextFactory<T>, scope providers, accessors, and distributed locking
Using EF Core Scopes
using IEFCoreScope<UmbracoDbContext> scope = _scopeProvider.CreateScope();
await scope.ExecuteWithContextAsync<MyEntity>(async dbContext =>
{
return await dbContext.MyEntities.FirstOrDefaultAsync(x => x.Id == id);
});
scope.Complete(); // Required to commitUmbracoDbContext Table Naming
All tables auto-prefixed with umbraco (see UmbracoDbContext.cs:85-88).
4. Scoping System
Scope Hierarchy
EFCoreScopeProvider<T>
├── Creates EFCoreScope<T> (normal scope)
└── Creates EFCoreDetachableScope<T> (for Deploy scenarios)
EFCoreScope<T>
├── Manages DbContext lifecycle
├── Handles transactions (BeginTransaction/Commit/Rollback)
├── Integrates with parent NPoco IScope when nested
└── Manages distributed locks via Locks propertyAmbient Scope Stack
Uses AsyncLocal<ConcurrentStack<T>> for thread-safe tracking. Scopes must be disposed in LIFO order (child before parent).
Transaction Integration
When an EF Core scope is created inside an NPoco scope:
- The EF Core scope reuses the parent's
DbTransaction - Transaction commit/rollback is delegated to the parent scope
- DbContext connects to the same connection as the parent
See EFCoreScope.cs:158-180 for transaction initialization logic.
5. Distributed Locking
SQL Server Locking
Table-level locks with REPEATABLEREAD hint. Timeout via SET LOCK_TIMEOUT. Requires ReadCommitted or higher.
SQLite Locking
Database-level locking (SQLite limitation). Read locks use snapshot isolation (WAL mode). Write locks are exclusive. Handles SQLITE_BUSY/LOCKED errors.
Lock Requirements
Requires active scope, transaction, and minimum ReadCommitted isolation.
6. Migration System
How Migrations Execute
UmbracoEFCoreComposerregistersEFCoreMigrationExecutor- Notification handler triggers on database creation
EFCoreMigrationExecutorfinds correctIMigrationProviderby provider name- Provider executes via
dbContext.Database.Migrate()
Adding New Migrations
Create equivalent migrations in both SqlServer and Sqlite provider projects using commands from section 2.
7. Project-Specific Notes
Known Technical Debt
Warning Suppressions (.csproj:9-14): IDE0270 (null checks), CS0108 (hiding members), CS1998 (async/await).
Detachable Scope (EFCoreDetachableScope.cs:93-94): TODO for Deploy integration, limited test coverage.
Circular Dependency Handling
SqlServerEFCoreDistributedLockingMechanism uses Lazy> to break circular dependency.
StaticServiceProvider Usage
Fallback for design-time EF tooling (migrations) and startup edge cases when DI unavailable.
OpenIddict Integration
UmbracoEFCoreComposer configures OpenIddict via options.UseOpenIddict() (see Composition/UmbracoEFCoreComposer.cs:22-36).
Vulnerable Dependency Override
Top-level dependency on Microsoft.Extensions.Caching.Memory overrides vulnerable EF Core transitive dependency (.csproj:18-19).
InternalsVisibleTo
Umbraco.Tests.Integration has access to internal types.
Quick Reference
Essential Files
| File | Purpose |
|---|---|
UmbracoDbContext.cs |
Base DbContext with table prefixing |
EFCoreScope.cs |
Main scope implementation |
EFCoreScopeProvider.cs |
Scope factory |
UmbracoEFCoreServiceCollectionExtensions.cs |
AddUmbracoDbContext<T> extension |
UmbracoEFCoreComposer.cs |
DI registration |
Key Interfaces
| Interface | Purpose |
|---|---|
IEFCoreScope |
Scope contract with ExecuteWithContextAsync |
IEFCoreScopeProvider |
Creates/manages scopes |
IEFCoreScopeAccessor |
Access ambient scope |
IMigrationProvider |
Provider-specific migration execution |
IMigrationProviderSetup |
DbContext options setup per provider |
Provider Constants
Constants.ProviderNames.SQLServer = "Microsoft.Data.SqlClient"
Constants.ProviderNames.SQLLite = "Microsoft.Data.Sqlite"Related Projects
Umbraco.Cms.Persistence.EFCore.SqlServer- SQL Server implementationUmbraco.Cms.Persistence.EFCore.Sqlite- SQLite implementationUmbraco.Infrastructure- Contains NPoco scoping that this integrates with
File: src/Umbraco.Cms.Persistence.EFCore.Sqlite/CLAUDE.md
Umbraco.Cms.Persistence.EFCore.Sqlite
SQLite-specific EF Core provider for Umbraco CMS. Contains SQLite migrations and provider setup for the EF Core persistence layer.
Project Type: Class Library (NuGet package)
Target Framework: net10.0
Dependencies: Umbraco.Cms.Persistence.EFCore
1. Architecture
Project Purpose
This is a thin provider project that implements SQLite-specific functionality for the EF Core persistence layer:
- Migration Provider - Executes SQLite-specific migrations
- Migration Provider Setup - Configures DbContext to use SQLite (incl. transient-error retry)
- Migrations - SQLite-specific migration files for OpenIddict tables
- Retrying Execution Strategy - Retries transient SQLite lock errors on EF Core operations
Folder Structure
Umbraco.Cms.Persistence.EFCore.Sqlite/
├── Migrations/
│ ├── 20230622183638_InitialCreate.cs # No-op (NPoco creates tables)
│ ├── 20230807123456_AddOpenIddict.cs # OpenIddict tables
│ ├── 20240403141051_UpdateOpenIddictToV5.cs # OpenIddict v5 schema
│ ├── 20251006140958_UpdateOpenIddictToV7.cs # OpenIddict v7 (no-op for SQLite)
│ └── UmbracoDbContextModelSnapshot.cs # Current model state
├── EFCoreSqliteComposer.cs # DI registration
├── SqliteMigrationProvider.cs # IMigrationProvider impl
├── SqliteMigrationProviderSetup.cs # IMigrationProviderSetup impl
└── SqliteRetryingExecutionStrategy.cs # IExecutionStrategy for transient lock errorsRelationship with Parent Project
This project extends Umbraco.Cms.Persistence.EFCore:
- Implements
IMigrationProviderinterface defined in parent - Implements
IMigrationProviderSetupinterface defined in parent - Uses
UmbracoDbContextfrom parent project - Provider name:
Microsoft.Data.Sqlite(fromConstants.ProviderNames.SQLLite)
2. Commands
For Git workflow and build commands, see repository root.
For EF Core migration commands, see parent project (substitute -p src/Umbraco.Cms.Persistence.EFCore.Sqlite).
3. Key Components
EFCoreSqliteComposer (line 10-14)
Registers IMigrationProvider and IMigrationProviderSetup for SQLite.
SqliteMigrationProvider
MigrateAsync(EFCoreMigration)- Runs specific named migrationMigrateAllAsync()- Runs all pending migrations- Critical: Cannot run
MigrateAllAsyncwhen transaction is active (line 26-29)
SqliteMigrationProviderSetup (line 11-14)
Configures DbContextOptionsBuilder with UseSqlite, the migrations assembly, and theSqliteRetryingExecutionStrategy (see below). Invoked fromUmbracoDbContext.ConfigureOptions for every UmbracoDbContext instance, so all EF Core
access to the Umbraco database (including OpenIddict's token store) inherits the retry.
SqliteRetryingExecutionStrategy
Custom Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy that retries on transient
SQLite errors (SQLITE_BUSY, SQLITE_LOCKED) using SqliteExceptionExtensions.IsBusyOrLocked
from the parent project. Defaults inherit ExecutionStrategy.DefaultMaxRetryCount (6) andExecutionStrategy.DefaultMaxDelay (30s), giving a ~56-second retry budget — see the class's
XML doc for the rationale and the unattended-upgrade escape hatch for very long migrations.
Added to resolve issue #22939 (OpenIddict token reads failing during long migrations).
4. Migrations
Migration History
| Migration | Date | Purpose |
|---|---|---|
InitialCreate |
2023-06-22 | No-op - NPoco creates base tables |
AddOpenIddict |
2023-08-07 | Creates OpenIddict tables (Applications, Tokens, Authorizations, Scopes) |
UpdateOpenIddictToV5 |
2024-04-03 | Schema updates for OpenIddict v5 |
UpdateOpenIddictToV7 |
2025-10-06 | No-op for SQLite (no schema changes needed) |
OpenIddict Tables Created
All tables prefixed with umbraco:
umbracoOpenIddictApplications- OAuth client applicationsumbracoOpenIddictAuthorizations- User authorizationsumbracoOpenIddictScopes- OAuth scopesumbracoOpenIddictTokens- Access/refresh tokens
SQLite-Specific Notes
- TEXT column type - SQLite uses TEXT for all strings (no varchar)
- InitialCreate is no-op - NPoco creates base tables, EF Core manages OpenIddict only
- v7 migration is no-op - SQLite didn't require schema changes that SQL Server needed
Adding New Migrations
- Configure SQLite connection string in
src/Umbraco.Web.UI/appsettings.json - Run migration command from repository root (see parent project docs)
- Critical: Also add equivalent migration to
Umbraco.Cms.Persistence.EFCore.SqlServer - Update
SqliteMigrationProvider.GetMigrationType()switch (line 34-42) if adding named migrations
5. Project-Specific Notes
Named Migration Mapping
SqliteMigrationProvider.GetMigrationType() (line 34-42) maps EFCoreMigration enum to migration types. When adding named migrations, update both the parent project's enum and this switch.
Auto-Generated Files
UmbracoDbContextModelSnapshot.cs is regenerated by EF Core - do not edit manually.
Quick Reference
Essential Files
| File | Purpose |
|---|---|
SqliteMigrationProvider.cs |
Migration execution |
SqliteMigrationProviderSetup.cs |
DbContext configuration (UseSqlite + retry strategy) |
SqliteRetryingExecutionStrategy.cs |
Retry on transient SQLite BUSY/LOCKED errors |
EFCoreSqliteComposer.cs |
DI registration |
Migrations/*.cs |
Migration files |
Provider Name
Constants.ProviderNames.SQLLite = "Microsoft.Data.Sqlite"
Related Projects
- Parent:
Umbraco.Cms.Persistence.EFCore- Interfaces and base DbContext - Sibling:
Umbraco.Cms.Persistence.EFCore.SqlServer- SQL Server equivalent
File: src/Umbraco.Cms.Persistence.EFCore.SqlServer/CLAUDE.md
Umbraco.Cms.Persistence.EFCore.SqlServer
SQL Server-specific EF Core provider for Umbraco CMS. Contains SQL Server migrations and provider setup for the EF Core persistence layer.
Project Type: Class Library (NuGet package)
Target Framework: net10.0
Dependencies: Umbraco.Cms.Persistence.EFCore
1. Architecture
Project Purpose
This is a thin provider project that implements SQL Server-specific functionality for the EF Core persistence layer:
- Migration Provider - Executes SQL Server-specific migrations
- Migration Provider Setup - Configures DbContext to use SQL Server
- Migrations - SQL Server-specific migration files for OpenIddict tables
Folder Structure
Umbraco.Cms.Persistence.EFCore.SqlServer/
├── Migrations/
│ ├── 20230622184303_InitialCreate.cs # No-op (NPoco creates tables)
│ ├── 20230807654321_AddOpenIddict.cs # OpenIddict tables
│ ├── 20240403140654_UpdateOpenIddictToV5.cs # OpenIddict v5 schema changes
│ ├── 20251006140751_UpdateOpenIddictToV7.cs # Token Type column expansion
│ └── UmbracoDbContextModelSnapshot.cs # Current model state
├── EFCoreSqlServerComposer.cs # DI registration
├── SqlServerMigrationProvider.cs # IMigrationProvider impl
└── SqlServerMigrationProviderSetup.cs # IMigrationProviderSetup implRelationship with Parent Project
This project extends Umbraco.Cms.Persistence.EFCore:
- Implements
IMigrationProviderinterface defined in parent - Implements
IMigrationProviderSetupinterface defined in parent - Uses
UmbracoDbContextfrom parent project - Provider name:
Microsoft.Data.SqlClient(fromConstants.ProviderNames.SQLServer)
2. Commands
For Git workflow and build commands, see repository root.
For EF Core migration commands, see parent project (substitute -p src/Umbraco.Cms.Persistence.EFCore.SqlServer).
3. Key Components
EFCoreSqlServerComposer (line 10-14)
Registers SQL Server implementations of IMigrationProvider and IMigrationProviderSetup.
SqlServerMigrationProvider
Executes migrations via MigrateAsync(EFCoreMigration) or MigrateAllAsync(). Unlike SQLite sibling, no transaction check needed (SQL Server handles concurrent migrations natively).
SqlServerMigrationProviderSetup (line 11-14)
Configures DbContextOptionsBuilder with UseSqlServer and migrations assembly.
4. Migrations
Migration History
| Migration | Date | Purpose |
|---|---|---|
InitialCreate |
2023-06-22 | No-op - NPoco creates base tables |
AddOpenIddict |
2023-08-07 | Creates OpenIddict tables |
UpdateOpenIddictToV5 |
2024-04-03 | Renames Type→ClientType, adds ApplicationType/JsonWebKeySet/Settings |
UpdateOpenIddictToV7 |
2025-10-06 | Expands Token.Type from nvarchar(50) to nvarchar(150) |
OpenIddict Tables Created
Prefixed with umbraco: Applications, Authorizations, Scopes, Tokens (see SQLite sibling for details).
SQL Server-Specific Differences from SQLite
- nvarchar types - Uses
nvarchar(n)andnvarchar(max)instead of TEXT - v5 migration has actual changes - Column rename and additions (SQLite handled differently)
- v7 migration has schema change - Token.Type column expanded (SQLite didn't need this)
Adding New Migrations
- Configure SQL Server connection string in
src/Umbraco.Web.UI/appsettings.json - Run migration command from repository root (see parent project docs)
- Critical: Also add equivalent migration to
Umbraco.Cms.Persistence.EFCore.Sqlite - Update
SqlServerMigrationProvider.GetMigrationType()switch (line 27-35) if adding named migrations
5. Project-Specific Notes
Named Migration Mapping
SqlServerMigrationProvider.GetMigrationType() (line 27-35) maps EFCoreMigration enum to migration types. Update both parent enum and this switch when adding named migrations.
Transaction Handling
SQL Server handles concurrent migrations natively - no transaction check needed (unlike SQLite).
Auto-Generated Files
UmbracoDbContextModelSnapshot.cs is regenerated by EF Core - do not edit manually.
Quick Reference
Essential Files
| File | Purpose |
|---|---|
SqlServerMigrationProvider.cs |
Migration execution |
SqlServerMigrationProviderSetup.cs |
DbContext configuration |
EFCoreSqlServerComposer.cs |
DI registration |
Migrations/*.cs |
Migration files |
Provider Name
Constants.ProviderNames.SQLServer = "Microsoft.Data.SqlClient"
Related Projects
- Parent:
Umbraco.Cms.Persistence.EFCore- Interfaces and base DbContext - Sibling:
Umbraco.Cms.Persistence.EFCore.Sqlite- SQLite equivalent
File: src/Umbraco.Cms.Persistence.Sqlite/CLAUDE.md
Umbraco.Cms.Persistence.Sqlite
SQLite database provider for Umbraco CMS using NPoco ORM. Provides SQLite-specific SQL syntax, type mappers, distributed locking, and connection interceptors.
Project Type: Class Library (NuGet package)
Target Framework: net10.0
Dependencies: Umbraco.Infrastructure, Microsoft.Data.Sqlite
Note: This is the legacy NPoco-based SQLite provider. For EF Core SQLite support, see Umbraco.Cms.Persistence.EFCore.Sqlite.
1. Architecture
Project Purpose
This project provides complete SQLite support for Umbraco's NPoco-based persistence layer:
- SQL Syntax Provider - SQLite-specific SQL generation and schema operations
- Type Mappers - GUID, decimal, date/time mapping for SQLite's type system
- Distributed Locking - SQLite-specific locking using WAL mode
- Connection Interceptors - Deferred transactions, MiniProfiler, retry policies
- Bulk Insert - SQLite-optimized record insertion
Folder Structure
Umbraco.Cms.Persistence.Sqlite/
├── Interceptors/
│ ├── SqliteAddMiniProfilerInterceptor.cs # MiniProfiler integration
│ ├── SqliteAddPreferDeferredInterceptor.cs # Deferred transaction wrapper
│ ├── SqliteAddRetryPolicyInterceptor.cs # Transient error retry
│ └── SqliteConnectionInterceptor.cs # Base interceptor
├── Mappers/
│ ├── SqliteGuidScalarMapper.cs # GUID as TEXT mapping
│ ├── SqlitePocoDateAndTimeOnlyMapper.cs # Date/Time as TEXT
│ ├── SqlitePocoDecimalMapper.cs # Decimal as TEXT (lossless)
│ └── SqlitePocoGuidMapper.cs # GUID for NPoco
├── Services/
│ ├── SqliteBulkSqlInsertProvider.cs # Bulk insert via transactions
│ ├── SqliteDatabaseCreator.cs # Database initialization
│ ├── SqliteDatabaseProviderMetadata.cs # Provider info
│ ├── SqliteDistributedLockingMechanism.cs # WAL-based locking
│ ├── SqliteExceptionExtensions.cs # Error code helpers
│ ├── SqlitePreferDeferredTransactionsConnection.cs # Deferred tx wrapper
│ ├── SqliteSpecificMapperFactory.cs # Type mapper factory
│ ├── SqliteSyntaxProvider.cs # SQL syntax (481 lines)
│ └── SqliteTransientErrorDetectionStrategy.cs # BUSY/LOCKED detection
├── Constants.cs # Provider name constant
├── SqliteComposer.cs # DI auto-registration
└── UmbracoBuilderExtensions.cs # AddUmbracoSqliteSupport()Auto-Registration
When this assembly is referenced, SqliteComposer automatically registers all SQLite services via the AddUmbracoSqliteSupport() extension method.
2. Commands
For Git workflow and build commands, see repository root.
3. Key Components
SqliteSyntaxProvider (Services/SqliteSyntaxProvider.cs)
The core SQL syntax provider implementing ISqlSyntaxProvider. Key characteristics:
- All strings stored as TEXT with
COLLATE NOCASE(case-insensitive) - GUIDs stored as TEXT (no native GUID support)
- Decimals stored as TEXT to avoid REAL precision loss (line 51)
- Default isolation level:
IsolationLevel.Serializable(line 66-67) - No identity insert support (line 73)
- No clustered index support (line 76)
- LIMIT instead of TOP for pagination (line 273-278)
- AUTOINCREMENT required to prevent magic ID issues with negative IDs (line 224-228)
SqliteDistributedLockingMechanism (Services/SqliteDistributedLockingMechanism.cs)
Database-level locking using SQLite WAL mode:
- Read locks: Snapshot isolation via WAL (mostly no-op, just validates transaction exists)
- Write locks: Only one writer at a time (entire database locked)
- Timeout handling: Uses
CommandTimeoutfor busy-wait (line 163) - Error handling: Catches
SQLITE_BUSYandSQLITE_LOCKEDerrors
SqlitePreferDeferredTransactionsConnection (Services/SqlitePreferDeferredTransactionsConnection.cs)
Wraps SqliteConnection to force deferred transactions:
// Line 33-34: The critical behavior
protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel)
=> _inner.BeginTransaction(isolationLevel, true); // <-- The important bitThis prevents immediate write locks when beginning transactions, allowing multiple readers.
Connection Interceptors
- SqliteAddPreferDeferredInterceptor - Wraps connections with deferred transaction support
- SqliteAddMiniProfilerInterceptor - Adds profiling for development
- SqliteAddRetryPolicyInterceptor - Retries on transient SQLite errors
4. SQLite Type Mapping
SQLite has limited types. Umbraco maps .NET types as follows:
| .NET Type | SQLite Type | Notes |
|---|---|---|
int, long, bool |
INTEGER | Native support |
string |
TEXT COLLATE NOCASE | Case-insensitive |
Guid |
TEXT | Stored as string representation |
DateTime, DateTimeOffset |
TEXT | ISO format string |
decimal |
TEXT | Prevents REAL precision loss |
double, float |
REAL | Native support |
byte[] |
BLOB | Native support |
5. Project-Specific Notes
Database Creation Prevention
UmbracoBuilderExtensions.cs (line 49-64) prevents accidental database file creation:
// Changes ReadWriteCreate mode to ReadWrite only
if (connectionStringBuilder.Mode == SqliteOpenMode.ReadWriteCreate)
{
connectionStringBuilder.Mode = SqliteOpenMode.ReadWrite;
}This ensures the database must exist before Umbraco connects.
WAL Mode Locking
SQLite with WAL (Write-Ahead Logging) journal mode:
- Multiple readers can access snapshots concurrently
- Only one writer at a time (database-level lock)
- Write lock uses
umbracoLocktable with UPDATE to acquire
Bulk Insert Implementation
SqliteBulkSqlInsertProvider (line 38-60) doesn't use true bulk copy (SQLite doesn't support it). Instead:
- Wraps inserts in a transaction if not already in one
- Inserts records one-by-one with
database.Insert() - Completes transaction
This is slower than SQL Server's SqlBulkCopy but safe for SQLite.
Known Technical Debt
- Warning Suppression (
.csproj:8-12):CS0114- hiding inherited members needs fixing - TODO in SqliteSyntaxProvider (line 178):
TryGetDefaultConstraintnot implemented for SQLite
Differences from SQL Server Provider
| Feature | SQLite | SQL Server |
|---|---|---|
| Bulk Insert | Transaction + individual inserts | SqlBulkCopy |
| Locking | Database-level (WAL) | Row-level |
| Identity Insert | Not supported | Supported |
| Clustered Indexes | Not supported | Supported |
| TOP clause | LIMIT | TOP |
| Decimal | TEXT (lossless) | DECIMAL |
| GUID | TEXT | UNIQUEIDENTIFIER |
Quick Reference
Essential Files
| File | Purpose |
|---|---|
SqliteSyntaxProvider.cs |
Core SQL syntax provider |
SqliteDistributedLockingMechanism.cs |
Database locking |
UmbracoBuilderExtensions.cs |
DI registration |
SqlitePreferDeferredTransactionsConnection.cs |
Deferred tx support |
Provider Name
Constants.ProviderName = "Microsoft.Data.Sqlite"
Registered Services
All registered via TryAddEnumerable (allowing multiple providers):
ISqlSyntaxProvider→SqliteSyntaxProviderIBulkSqlInsertProvider→SqliteBulkSqlInsertProviderIDatabaseCreator→SqliteDatabaseCreatorIProviderSpecificMapperFactory→SqliteSpecificMapperFactoryIDatabaseProviderMetadata→SqliteDatabaseProviderMetadataIDistributedLockingMechanism→SqliteDistributedLockingMechanismIProviderSpecificInterceptor→ Three interceptors
Related Projects
- Sibling:
Umbraco.Cms.Persistence.SqlServer- SQL Server NPoco provider - EF Core:
Umbraco.Cms.Persistence.EFCore.Sqlite- EF Core SQLite provider
File: src/Umbraco.Cms.Persistence.SqlServer/CLAUDE.md
Umbraco.Cms.Persistence.SqlServer
SQL Server database provider for Umbraco CMS using NPoco ORM. Provides SQL Server-specific SQL syntax, bulk copy operations, distributed locking, and Azure SQL transient fault handling.
Project Type: Class Library (NuGet package)
Target Framework: net10.0
Dependencies: Umbraco.Infrastructure, NPoco.SqlServer, Microsoft.Data.SqlClient
Note: This is the legacy NPoco-based SQL Server provider. For EF Core SQL Server support, see Umbraco.Cms.Persistence.EFCore.SqlServer.
1. Architecture
Project Purpose
This project provides complete SQL Server support for Umbraco's NPoco-based persistence layer:
- SQL Syntax Provider - SQL Server-specific SQL generation and schema operations
- Bulk Insert - True
SqlBulkCopyfor high-performance batch inserts - Distributed Locking - Row-level locking with
REPEATABLEREADhints - Fault Handling - Azure SQL and network transient error retry policies
- LocalDB Support - SQL Server LocalDB for development
Folder Structure
Umbraco.Cms.Persistence.SqlServer/
├── Dtos/
│ ├── ColumnInSchemaDto.cs # Schema query results
│ ├── ConstraintPerColumnDto.cs
│ ├── ConstraintPerTableDto.cs
│ ├── DefaultConstraintPerColumnDto.cs
│ └── DefinedIndexDto.cs
├── FaultHandling/
│ ├── RetryPolicyFactory.cs # Creates retry policies
│ └── Strategies/
│ ├── NetworkConnectivityErrorDetectionStrategy.cs
│ ├── SqlAzureTransientErrorDetectionStrategy.cs # Azure SQL error codes
│ └── ThrottlingCondition.cs # Azure throttling decode
├── Interceptors/
│ ├── SqlServerAddMiniProfilerInterceptor.cs
│ ├── SqlServerAddRetryPolicyInterceptor.cs
│ └── SqlServerConnectionInterceptor.cs # Base interceptor
├── Services/
│ ├── BulkDataReader.cs # Base bulk reader
│ ├── MicrosoftSqlSyntaxProviderBase.cs # Shared SQL syntax
│ ├── PocoDataDataReader.cs # NPoco bulk reader
│ ├── SqlServerBulkSqlInsertProvider.cs # SqlBulkCopy wrapper
│ ├── SqlServerDatabaseCreator.cs
│ ├── SqlServerDistributedLockingMechanism.cs
│ ├── SqlServerSpecificMapperFactory.cs
│ ├── SqlServerSyntaxProvider.cs # Main syntax provider
│ ├── SqlAzureDatabaseProviderMetadata.cs
│ ├── SqlLocalDbDatabaseProviderMetadata.cs
│ └── SqlServerDatabaseProviderMetadata.cs
├── Constants.cs # Provider name
├── LocalDb.cs # LocalDB management (~1,100 lines)
├── NPocoSqlServerDatabaseExtensions.cs # NPoco bulk extensions config
├── SqlServerComposer.cs # DI auto-registration
└── UmbracoBuilderExtensions.cs # AddUmbracoSqlServerSupport()Auto-Registration
When this assembly is referenced, SqlServerComposer automatically registers all SQL Server services via AddUmbracoSqlServerSupport().
Provider Name Migration
Auto-migrates legacy System.Data.SqlClient to Microsoft.Data.SqlClient:
Line 53-59: UmbracoBuilderExtensions.cs updates connection string provider name
if (options.ProviderName == "System.Data.SqlClient")
{
options.ProviderName = Constants.ProviderName; // Microsoft.Data.SqlClient
}2. Commands
For Git workflow and build commands, see repository root.
3. Key Components
SqlServerDistributedLockingMechanism (Services/SqlServerDistributedLockingMechanism.cs)
Row-level locking using REPEATABLEREAD table hints:
- Read locks (line 147):
SELECT value FROM umbracoLock WITH (REPEATABLEREAD) WHERE id=@id - Write locks (line 182-183):
UPDATE umbracoLock WITH (REPEATABLEREAD) SET value = ... WHERE id=@id - Timeout: Uses
SET LOCK_TIMEOUT {milliseconds}(lines 149, 185) - Error handling: Catches SQL error 1222 (lock timeout) → throws
DistributedReadLockTimeoutExceptionorDistributedWriteLockTimeoutException - Minimum isolation: Requires
ReadCommittedor higher (lines 141-145, 176-180)
SqlServerBulkSqlInsertProvider (Services/SqlServerBulkSqlInsertProvider.cs)
True bulk insert using SqlBulkCopy:
// Line 61-68: SqlBulkCopy configuration
new SqlBulkCopy(tConnection, SqlBulkCopyOptions.Default, tTransaction)
{
BulkCopyTimeout = 0, // No timeout (uses connection timeout)
DestinationTableName = tableName,
BatchSize = 4096, // Consistent with NPoco
}Key features:
- Streams data via
PocoDataDataReader<T>(line 69) - avoids in-memory DataTable - Explicit column mappings by name (lines 74-77) - prevents column order mismatches
- Returns actual count inserted (line 80) - NPoco's
InsertBulkdoesn't provide this
SqlAzureTransientErrorDetectionStrategy (FaultHandling/Strategies/)
Detects transient Azure SQL errors for retry:
| Error Code | Description |
|---|---|
| 40501 | Service busy (throttling) |
| 10928/10929 | Resource limits reached |
| 10053/10054 | Transport-level errors |
| 10060 | Connection timeout |
| 40197/40540/40143 | Service processing errors |
| 40613 | Database not available |
| 233 | Connection initialization error |
| 64 | Login process error |
SqlServerSyntaxProvider (Services/SqlServerSyntaxProvider.cs)
SQL Server version detection and syntax generation:
- Engine editions: Desktop, Standard, Enterprise, Express, Azure (lines 23-31)
- SQL Server versions: V7 through V2019, plus Azure (lines 33-47)
- Default isolation:
ReadCommitted(line 69) - Azure detection:
ServerVersion?.IsAzuredeterminesDbProvider(line 67) - Version detection: Populated via
GetDbProviderManifest()by querying SQL Server metadata
4. LocalDB Support
LocalDb.cs (~1,100 lines) provides comprehensive SQL Server LocalDB management:
- Database creation/deletion
- Instance management
- File path handling
- Connection string generation
Known issues (from TODO comments):
- Line 358: Stale database handling not implemented
- Line 359: File name assumptions may not always be correct
5. Differences from SQLite Provider
| Feature | SQL Server | SQLite |
|---|---|---|
| Bulk Insert | SqlBulkCopy (true bulk) |
Transaction + individual inserts |
| Locking | Row-level with REPEATABLEREAD hints |
Database-level (WAL mode) |
| Types | Native (NVARCHAR, DECIMAL, UNIQUEIDENTIFIER) | TEXT for most types |
| Transient Retry | Azure SQL error codes + network errors | BUSY/LOCKED retry only |
| LocalDB | Full support (~1,100 lines) | N/A |
6. Project-Specific Notes
InternalsVisibleTo
Test projects have access to internal types:
<InternalsVisibleTo>Umbraco.Tests.Integration</InternalsVisibleTo>
<InternalsVisibleTo>Umbraco.Tests.UnitTests</InternalsVisibleTo>Known Technical Debt
Warning Suppressions (
.csproj:8-22): Multiple analyzer warnings suppressed- StyleCop: SA1405, SA1121, SA1117
- IDE: 1006 (naming), 0270/0057/0054/0048 (simplification)
- CS0618 (obsolete usage), CS1574 (XML comments)
BulkInsert TODO (
SqlServerBulkSqlInsertProvider.cs:44-46): CustomSqlBulkCopyimplementation used because NPoco'sInsertBulkdoesn't return record count. Performance comparison vs NPoco's DataTable approach pending.LocalDB TODOs (
LocalDb.cs:358-359): Stale database cleanup and file name assumption handling incomplete.
NPoco Bulk Extensions
NPocoSqlServerDatabaseExtensions.ConfigureNPocoBulkExtensions() is called during registration (line 50) to configure NPoco's SQL Server bulk operations.
Three Provider Metadata Types
Different metadata for different SQL Server variants:
SqlServerDatabaseProviderMetadata- Standard SQL ServerSqlLocalDbDatabaseProviderMetadata- LocalDBSqlAzureDatabaseProviderMetadata- Azure SQL
Quick Reference
Essential Files
| File | Purpose |
|---|---|
SqlServerSyntaxProvider.cs |
Core SQL syntax provider |
SqlServerDistributedLockingMechanism.cs |
Row-level locking |
SqlServerBulkSqlInsertProvider.cs |
SqlBulkCopy wrapper |
SqlAzureTransientErrorDetectionStrategy.cs |
Azure retry logic |
LocalDb.cs |
LocalDB management |
Provider Name
Constants.ProviderName = "Microsoft.Data.SqlClient"
Registered Services
All registered via TryAddEnumerable:
ISqlSyntaxProvider→SqlServerSyntaxProviderIBulkSqlInsertProvider→SqlServerBulkSqlInsertProviderIDatabaseCreator→SqlServerDatabaseCreatorIProviderSpecificMapperFactory→SqlServerSpecificMapperFactoryIDatabaseProviderMetadata→ Three variants (SqlServer, LocalDb, Azure)IDistributedLockingMechanism→SqlServerDistributedLockingMechanismIProviderSpecificInterceptor→ MiniProfiler and RetryPolicy interceptors
Related Projects
- Sibling:
Umbraco.Cms.Persistence.Sqlite- SQLite NPoco provider - EF Core:
Umbraco.Cms.Persistence.EFCore.SqlServer- EF Core SQL Server provider
File: src/Umbraco.Cms.StaticAssets/CLAUDE.md
Umbraco.Cms.StaticAssets
Static assets and Razor views for Umbraco CMS. This is a Razor Class Library (RCL) that packages all backoffice, login, and website assets for deployment.
Project Type: Razor Class Library (NuGet package)
Target Framework: net10.0
SDK: Microsoft.NET.Sdk.Razor
1. Architecture
Project Purpose
This project packages all static assets required for Umbraco CMS at runtime:
- Razor Views - Server-rendered pages for backoffice, login, and error pages
- Static Web Assets - JavaScript, CSS, fonts, images served at runtime
- Build Integration - MSBuild targets that compile TypeScript/frontend projects on demand
Key Characteristic
No C# Source Code - This is a pure asset packaging project. All functionality comes from referenced projects (Umbraco.Cms.Api.Management, Umbraco.Web.Website).
Folder Structure
Umbraco.Cms.StaticAssets/
├── umbraco/ # Razor Views (server-rendered)
│ ├── UmbracoBackOffice/
│ │ └── Index.cshtml # Backoffice SPA shell
│ ├── UmbracoLogin/
│ │ └── Index.cshtml # Login page
│ └── UmbracoWebsite/
│ ├── NoNodes.cshtml # "No published content" page
│ ├── NotFound.cshtml # 404 error page
│ └── Maintenance.cshtml # Maintenance mode page
│
├── wwwroot/ # Static Web Assets
│ ├── App_Plugins/
│ │ └── Umbraco.BlockGridEditor.DefaultCustomViews/ # Block grid demo templates
│ └── umbraco/
│ ├── assets/ # Logos and branding (see README.md)
│ ├── backoffice/ # Built backoffice SPA (from Umbraco.Web.UI.Client)
│ │ ├── apps/ # Application modules
│ │ ├── assets/ # Fonts, language files
│ │ ├── css/ # Themes and stylesheets
│ │ └── monaco-editor/ # Code editor assets
│ ├── login/ # Built login SPA (from Umbraco.Web.UI.Login)
│ └── website/ # Frontend website assets (fonts, CSS)
│
└── Umbraco.Cms.StaticAssets.csproj # Build configurationProject Dependencies
<ProjectReference Include="..\Umbraco.Cms.Api.Management\Umbraco.Cms.Api.Management.csproj" />
<ProjectReference Include="..\Umbraco.Web.Website\Umbraco.Web.Website.csproj" />2. Commands
For Git workflow and build commands, see repository root.
3. Key Components
Razor Views
| View | Purpose |
|---|---|
UmbracoBackOffice/Index.cshtml |
Backoffice SPA entry point with `` web component |
UmbracoLogin/Index.cshtml |
Login page with `` web component |
UmbracoWebsite/NoNodes.cshtml |
Welcome page when no content published |
UmbracoWebsite/NotFound.cshtml |
404 error page (debug info in debug mode) |
UmbracoWebsite/Maintenance.cshtml |
Maintenance mode during upgrades |
Backoffice Index View (umbraco/UmbracoBackOffice/Index.cshtml)
Key injected services:
IBackOfficePathGenerator- Generates backoffice URL pathsIPackageManifestService- Package manifest discoveryIJsonSerializer- JSON serialization for import mapsIProfilerHtml- MiniProfiler integration
Key features:
- Import Maps (line 35):
Html.BackOfficeImportMapScriptAsync()generates JavaScript module import maps - Debug Mode (line 16, 63-66):
?umbDebug=truequery param enables profiler - NoScript Fallback (lines 40-60): Displays message if JavaScript disabled
Login View (umbraco/UmbracoLogin/Index.cshtml)
Configures `` web component with attributes:
return-url- Redirect after loginlogo-image/background-image- Branding URLs fromBackOfficeGraphicsControllerusername-is-email- FromSecuritySettingsallow-user-invite/allow-password-reset- Email capability checkdisable-local-login- External login provider configuration
4. Build System
Frontend Build Integration (csproj lines 36-90)
The .csproj contains MSBuild targets that automatically build frontend projects when assets are missing.
Backoffice Build (lines 36-90):
BackofficeProjectDirectory = ../Umbraco.Web.UI.Client/
BackofficeAssetsPath = wwwroot/umbraco/backofficeLogin Build (lines 102-148):
LoginProjectDirectory = ../Umbraco.Web.UI.Login/
LoginAssetsPath = wwwroot/umbraco/loginBuild Targets
| Target | Purpose |
|---|---|
BuildBackofficeStaticAssetsPreconditions |
Checks if Backoffice build needed (Visual Studio only) |
RestoreBackoffice |
Runs npm i if package-lock changed |
BuildBackoffice |
Runs npm run build:for:cms |
DefineBackofficeAssets |
Registers Backoffice assets with StaticWebAssets system |
CleanBackoffice |
Removes built Backoffice assets on dotnet clean |
BuildLoginStaticAssetsPreconditions |
Checks if Login build needed (Visual Studio only) |
RestoreLogin |
Runs npm i if Login's package-lock changed |
BuildLogin |
Runs npm run build in Login. Depends on RestoreBackoffice because Login's tsc walks Client's src/ via tsconfig path aliases and needs Client's node_modules populated for transitive lit/rxjs/UUI resolution |
DefineLoginAssets |
Registers Login assets with StaticWebAssets system |
CleanLogin |
Removes built Login assets on dotnet clean |
Build Conditions
- UmbracoBuild Variable: When set (CI/CD), skips frontend builds (pre-built assets expected)
- Visual Studio Detection:
'$(UmbracoBuild)' == ''indicates VS build - preserve.backoffice Marker: Skip clean if
preserve.backofficefile exists in solution root
5. Static Web Assets
Asset Categories
| Directory | Contents | Served At |
|---|---|---|
wwwroot/umbraco/assets/ |
Branding logos | Via BackOfficeGraphicsController API |
wwwroot/umbraco/backoffice/ |
Built backoffice SPA | /umbraco/backoffice/* |
wwwroot/umbraco/login/ |
Built login SPA | /umbraco/login/* |
wwwroot/umbraco/website/ |
Website assets (fonts, CSS) | /umbraco/website/* |
wwwroot/App_Plugins/ |
Block editor demo views | /App_Plugins/* |
Logo Assets (wwwroot/umbraco/assets/)
Documented in wwwroot/umbraco/assets/README.md (16 lines):
| File | Usage | API Endpoint |
|---|---|---|
logo.svg |
Backoffice and public sites | /umbraco/management/api/v1/security/back-office/graphics/logo |
logo_dark.svg |
Login screen (dark mode) | .../graphics/login-logo-alternative |
logo_light.svg |
Login screen (light mode) | .../graphics/login-logo |
logo_blue.svg |
Alternative branding | N/A |
Block Grid Demo Views (wwwroot/App_Plugins/Umbraco.BlockGridEditor.DefaultCustomViews/)
Pre-built AngularJS templates for block grid editor demos:
umbBlockGridDemoHeadlineBlock.htmlumbBlockGridDemoImageBlock.htmlumbBlockGridDemoRichTextBlock.htmlumbBlockGridDemoTwoColumnLayoutBlock.html
6. Project-Specific Notes
Static Web Asset Base Path
<StaticWebAssetBasePath>/</StaticWebAssetBasePath>Assets are served from root path, not under assembly name.
Compression Disabled
<CompressionEnabled>false</CompressionEnabled>Comment notes MapStaticAssets() is not used (yet).
Known Technical Debt
Warning Suppression (
.csproj:14-16):NU5123- File paths too long for NuGet package. TODO indicates files should be renamed.Excluded Content (
.csproj:31):wwwroot/umbraco/assets/README.mdexplicitly excluded from package.
Backoffice Localization
The backoffice includes language files for 25+ languages in wwwroot/umbraco/backoffice/assets/lang/:
- ar, bs, cs, cy, da, de, en, en-us, es, fr, he, hr, it, ja, ko, nb, nl, pl, pt, pt-br, ro, ru, sv, tr, uk, zh, zh-tw
Monaco Editor
Full Monaco code editor included at wwwroot/umbraco/backoffice/monaco-editor/ for rich code editing in backoffice.
Theming
CSS themes in wwwroot/umbraco/backoffice/css/:
umb-css.css- Main styleslight.css- UI library styles default styles (Light theme)dark.css- UUI Dark themehigh-contrast.css- UUI Accessibility themeumbraco-blockgridlayout.css- Block grid stylesrte-content.css- Rich text editor content styles
The UUI theme files (light.css, dark.css, high-contrast.css) are copied from @umbraco-ui/uui during the backoffice build; see Umbraco.Web.UI.Client/src/packages/core/themes/manifests.ts.
Quick Reference
Essential Files
| File | Purpose |
|---|---|
umbraco/UmbracoBackOffice/Index.cshtml |
Backoffice entry point |
umbraco/UmbracoLogin/Index.cshtml |
Login page |
wwwroot/umbraco/assets/README.md |
Asset documentation |
Umbraco.Cms.StaticAssets.csproj |
Build targets for frontend |
Related Projects
| Project | Relationship |
|---|---|
Umbraco.Web.UI.Client |
Source for backoffice assets (npm build) |
Umbraco.Web.UI.Login |
Source for login assets (npm build) |
Umbraco.Cms.Api.Management |
Razor view dependencies |
Umbraco.Web.Website |
Razor view dependencies |
Build Commands (Manual)
# Build backoffice assets (from Umbraco.Web.UI.Client)
cd src/Umbraco.Web.UI.Client
npm install
npm run build:for:cms
# Build login assets (from Umbraco.Web.UI.Login)
cd src/Umbraco.Web.UI.Login
npm install
npm run buildPreserve Assets During Clean
Create marker file to prevent asset deletion:
touch preserve.backoffice # In solution root
touch preserve.login # In solution rootFile: src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/assets/README.md
Umbraco static assets
The files in this directory are static assets that are used by the Umbraco backoffice, installer, public-facing sites, and login screen. These files are served by the Umbraco backend and are not intended to be used by the front-end of your website.
Structure
The assets are structured in the following way:
| Name | Description | Usage |
|---|---|---|
login.jpg |
The background image for the login screen. | /umbraco/management/api/v1/security/back-office/graphics/login-background |
logo.svg |
The Umbraco logo for the Backoffice and other public facing sites. | /umbraco/management/api/v1/security/back-office/graphics/logo |
logo_dark.svg |
The Umbraco logo in dark mode for the login screen. | /umbraco/management/api/v1/security/back-office/graphics/login-logo-alternative |
logo_light.svg |
The Umbraco logo in light mode for the login screen. | /umbraco/management/api/v1/security/back-office/graphics/login-logo |
All assets are linked up through the BackOfficeGraphicsController which uses the constants defined in ContentSettings.
File: src/Umbraco.Core/CLAUDE.md
Umbraco.Core Project Guide
Project Overview
Umbraco.Core is the foundational layer of Umbraco CMS, containing the core domain models, services interfaces, events/notifications system, and essential abstractions. This project has NO database implementation and minimal external dependencies - it focuses on defining contracts and business logic.
Package ID: Umbraco.Cms.Core
Namespace: Umbraco.Cms.Core
What Lives Here vs. Other Projects
- Umbraco.Core: Interfaces, models, notifications, service contracts, core business logic, configuration models
- Umbraco.Infrastructure: Concrete implementations (repositories, database access, file systems, concrete services)
- Umbraco.Web.Common: Web-specific functionality (controllers, middleware, routing)
- Umbraco.Cms.Api.Common: API endpoints and DTOs
Key Directory Structure
Core Domain Areas
/Models - Domain models and DTOs
├── /Entities - Base entity interfaces (IEntity, IUmbracoEntity, ITreeEntity)
├── /ContentEditing - Content editing models and DTOs
├── /ContentPublishing - Publishing-related models
├── /Membership - User, member, and group models
├── /PublishedContent - Published content abstractions
├── /DeliveryApi - Delivery API models
└── /Blocks - Block List/Grid models
/Services - Service interfaces (~237 files!)
├── /OperationStatus - Enums for service operation results (48 status types)
├── /ContentTypeEditing - Content type editing services
├── /Navigation - Navigation services
└── /ImportExport - Import/export services
/Notifications - Event notification system (100+ notification types)
/Events - Event infrastructure and handlersInfrastructure & Patterns
/Composing - Composer pattern for DI registration
/DependencyInjection - IUmbracoBuilder and DI extensions
/Scoping - Unit of work pattern (ICoreScopeProvider)
/Cache - Caching abstractions and refreshers
├── /Refreshers - Cache invalidation for distributed systems
└── /NotificationHandlers - Cache invalidation via notifications
/PropertyEditors - Property editor abstractions
├── /ValueConverters - Convert stored values to typed values
├── /Validation - Property validation infrastructure
└── /DeliveryApi - Delivery API property converters
/Persistence - Repository interfaces (no implementations!)
├── /Repositories - Repository contracts
└── /Querying - Query abstractionsSupporting Functionality
/Configuration - Configuration models and settings
├── /Models - Strongly-typed settings (50+ configuration classes)
└── /UmbracoSettings - Legacy Umbraco settings
/Extensions - Extension methods (100+ extension files)
/Mapping - Object mapping abstractions (IUmbracoMapper)
/Serialization - JSON serialization configuration
/Security - Security abstractions and authorization
/Routing - URL routing abstractions
/Templates - Template/view abstractions
/Webhooks - Webhook event system
/HealthChecks - Health check abstractions
/Telemetry - Telemetry/analytics
/Install & /Installer - Installation infrastructureCore Patterns and Conventions
1. Service Layer Pattern
Services follow a consistent structure:
// Interface defines contract (in Umbraco.Core)
public interface IContentService
{
IContent? GetById(Guid key);
Task<Attempt<IContent?, ContentEditingOperationStatus>> CreateAsync(...);
}
// Implementation lives in Umbraco.Core/Services/ (see root CLAUDE.md §5)
// Service operations return Attempt<T, TStatus> for typed resultsKey conventions:
- Interfaces in Umbraco.Core; most service implementations live in Umbraco.Core/Services/ too, moving to Umbraco.Infrastructure only when a concrete dependency requires it (see root CLAUDE.md §5)
- Use
Attemptfor operations that can fail with specific reasons - OperationStatus enums provide detailed failure reasons
- Services are registered via DI in builder extensions
2. Notification System (Event Handling)
Umbraco uses a notification pattern instead of traditional events:
// 1. Define notification (implements INotification)
public class ContentSavedNotification : INotification
{
public IEnumerable<IContent> SavedEntities { get; }
}
// 2. Create handler
public class MyNotificationHandler : INotificationHandler<ContentSavedNotification>
{
public void Handle(ContentSavedNotification notification)
{
// React to content being saved
}
}
// 3. Add to appropriate builder extensions
builder.AddNotificationHandler<ContentSavedNotification, MyNotificationHandler>();Notification types:
*SavingNotification- Before save (cancellable viaICancelableNotification)*SavedNotification- After save*DeletingNotification/*DeletedNotification*MovingNotification/*MovedNotification
Key interface: IEventAggregator - publishes notifications to handlers
Durable handlers vs. distributed-cache-only publishers
A handler that only implements INotificationHandler<T> / INotificationAsyncHandler<T>
is silently skipped when the notification is dispatched through a publisher restricted to
distributed-cache handlers — i.e. ScopedNotificationPublisher.
Umbraco Deploy installs exactly such a publisher on its restore/import scopes, and EventAggregator
filters handlers with .Where(x => x is TNotificationHandler).
If a handler must run for every change — especially durable database writes that back the
in-memory cache — implement IDistributedCacheNotificationHandler (sync) orIDistributedCacheAsyncNotificationHandler<T> (async) instead. These derive from the plain handler
interfaces, so the handler still fires on normal publishes and survives the distributed-cache-only
filter. No DI change is needed — filtering is by resolved instance type.
When you do this, ensure the handler is safe in the extra scopes it now runs in. Do not gate a write that
follows a local content commit on server role: the elected Subscriber role can be held by any instance,
including one serving the backoffice (see DocumentUrlService.SkipDatabaseWrites() for the split).
3. Composer Pattern (DI Registration)
Composers register services and configure the application, this is to make the system easily extendible by package developers and implementors.
Internally we only use this for temporary service registration for use in short lived code, for example migrations:
public class MyComposer : IComposer
{
public void Compose(IUmbracoBuilder builder)
{
// Register services
builder.Services.AddSingleton<IMyService, MyService>();
// Add to collections
builder.PropertyEditors().Add<MyPropertyEditor>();
// Register notification handlers
builder.AddNotificationHandler<ContentSavedNotification, MyHandler>();
}
}Composer ordering:
[ComposeBefore(typeof(OtherComposer))][ComposeAfter(typeof(OtherComposer))][Weight(100)]- lower runs first
4. Entity and Content Model Hierarchy
IEntity - Base: Id, Key, CreateDate, UpdateDate
└─ IUmbracoEntity - Adds: Name, CreatorId, ParentId, Path, Level, SortOrder
└─ ITreeEntity - Tree structure support
└─ IContentBase - Adds: Properties, ContentType, CultureInfos
├─ IContent - Documents (publishable)
├─ IMedia - Media items
└─ IMember - MembersKey interfaces:
IContentBase- Common base for all content items (properties, cultures)IContent- Documents with publishing workflowIContentType,IMediaType,IMemberType- Define structureIProperty- Individual property on contentIPropertyType- Property definition on content type
5. Scoping Pattern (Unit of Work)
public class MyService
{
private readonly ICoreScopeProvider _scopeProvider;
public void DoWork()
{
using ICoreScope scope = _scopeProvider.CreateCoreScope();
// Do database work
// Access repositories
scope.Complete(); // Commit transaction
}
}Key points:
- Scopes manage transactions and cache lifetime
- Must call
scope.Complete()to commit - Scopes can be nested (innermost controls transaction)
RepositoryCacheModecontrols caching behavior
6. Property Editors
Property editors define how data is edited and stored:
public class MyPropertyEditor : IDataEditor
{
public string Alias => "My.PropertyEditor";
public IDataValueEditor GetValueEditor()
{
return new MyDataValueEditor();
}
public IConfigurationEditor GetConfigurationEditor()
{
return new MyConfigurationEditor();
}
}Key interfaces:
IDataEditor- Property editor registrationIDataValueEditor- Value editing and conversionIPropertyIndexValueFactory- Search indexingIPropertyValueConverter- Convert stored values to typed values
7. Cache Refreshers (Distributed Cache)
For multi-server deployments, cache refreshers synchronize cache:
public class MyEntityCacheRefresher : CacheRefresherBase<MyEntityCacheRefresher>
{
public override Guid RefresherUniqueId => new Guid("...");
public override string Name => "My Entity Cache Refresher";
public override void RefreshAll()
{
// Clear all cache
}
public override void Refresh(int id)
{
// Clear cache for specific entity
}
}Important Base Classes and Interfaces
Must-Know Abstractions
Service Layer
IContentService- Content CRUD operationsIContentTypeService- Content type managementIMediaService- Media operationsIDataTypeService- Data type configurationIUserService- User managementILanguageService- LanguagesIDictionaryItemService- Dictionary itemsIRelationService- Entity relationships
Content Models
IContent/IContentBase- Document entitiesIContentType- Document type definitionIProperty/IPropertyType- Property definitionsIPublishedContent- Read-only published content
Infrastructure
ICoreScopeProvider- Unit of work / transactionsIEventAggregator- Notification publishingIUmbracoMapper- Object mappingIComposer/IUmbracoBuilder- DI registration
Entity Base Classes
EntityBase- Basic entity with Id, Key, datesTreeEntityBase- Adds Name, hierarchyBeingDirty/ICanBeDirty- Change tracking
Key Files and Constants
Essential Files
Constants.cs (and 40 Constants-*.cs files)
Constants.System.*- System-level constantsConstants.Security.*- Security and authorizationConstants.Conventions.*- Naming conventionsConstants.PropertyEditors.*- Built-in property editor aliasesConstants.ObjectTypes.*- Entity type GUIDs- Use these instead of magic strings!
Udi.cs / GuidUdi.cs / StringUdi.cs
- Umbraco Identifiers (like URIs):
umb://document/{guid} - Used throughout for entity references
UdiParser.Parse("umb://document/...")to parse
- Umbraco Identifiers (like URIs):
Attempt.cs and Attempt.cs
- Result pattern for operations that can fail
Attempt.Succeed(value)/Attempt.Fail<T>()Attempt<Content, ContentEditingOperationStatus>- typed result with status
Writing or reviewing a query with a
WHERE INon a runtime-sized collection? See "Avoiding the SQL Server 2100-parameter limit" in/src/Umbraco.Infrastructure/CLAUDE.md— that's where the full helper list (Constants.Sql.MaxParameterCount,InGroupsOf, NPoco'sFetchByGroups) and the decision rules live.
Configuration
Configuration models in /Configuration/Models:
ContentSettings- Content-related settingsGlobalSettings- Global Umbraco settingsSecuritySettings- Security configurationDeliveryApiSettings- Delivery API configuration- Access via
IOptionsMonitor<TSettings>
Only top-level settings are bound as options. A settings class is bound to configuration only when it has an explicit AddUmbracoOptions<T>() registration in UmbracoBuilder.Configuration.cs (the [UmbracoOptions] attribute alone does nothing — nothing scans for it). A nested settings class exposed as a property of another settings class (e.g. ContentSettings.Imaging, SecuritySettings.UserPassword) is populated as part of binding its parent — it is not independently bound. Do not inject a nested settings type directly as IOptions<TNested>/IOptionsSnapshot<TNested>: with no registration the options system silently hands back a default-constructed instance and all user configuration is ignored. Instead inject the parent and read the nested property (_contentSettings.Imaging). The UserPassword/MemberPassword settings that are separately registered are a legacy exception slated for removal (see the TODO (V18) in UmbracoBuilder.Configuration.cs); don't copy that pattern for new nested settings.
Dependencies
What Umbraco.Core Depends On
- Microsoft.Extensions.* - DI, Configuration, Logging, Caching, Options
- Microsoft.Extensions.Identity.Core - Identity infrastructure
- NO database dependencies
- NO web dependencies
What Depends on Umbraco.Core
- Umbraco.Infrastructure - Implements all Core interfaces
- Umbraco.PublishedCache.HybridCache - Published content caching
- Umbraco.Cms.Persistence.EFCore - EF Core persistence
- Umbraco.Cms.Api.Common - API infrastructure
- All higher-level Umbraco projects
Common Development Tasks
1. Creating a New Service
// 1. Define interface in Umbraco.Core/Services/IMyService.cs
public interface IMyService
{
Task<Attempt<MyResult, MyOperationStatus>> CreateAsync(...);
}
// 2. Define operation status in Services/OperationStatus/
public enum MyOperationStatus
{
Success,
NotFound,
ValidationFailed
}
// 3. Implement in Umbraco.Core/Services/ (or Umbraco.Infrastructure if it needs Infrastructure concerns)
// 4. Register in a Composer
builder.Services.AddScoped<IMyService, MyService>();2. Adding a Notification Handler
// 1. Identify notification (e.g., ContentSavingNotification)
// 2. Create handler
public class MyContentHandler : INotificationHandler<ContentSavingNotification>
{
public void Handle(ContentSavingNotification notification)
{
foreach (var content in notification.SavedEntities)
{
// Validate, modify, or react
}
// Cancel if needed (for cancellable notifications)
// notification.Cancel = true;
}
}
// 3. Register in Composer
builder.AddNotificationHandler<ContentSavingNotification, MyContentHandler>();3. Creating a Property Editor
// 1. Define in Umbraco.Core/PropertyEditors/
[DataEditor("My.Alias", "My Editor", "view")]
public class MyPropertyEditor : DataEditor
{
public MyPropertyEditor(IDataValueEditorFactory dataValueEditorFactory)
: base(dataValueEditorFactory)
{ }
protected override IDataValueEditor CreateValueEditor()
{
return DataValueEditorFactory.Create<MyValueEditor>(Attribute!);
}
}
// 2. Register in Composer
builder.PropertyEditors().Add<MyPropertyEditor>();4. Working with Content
public class MyService
{
private readonly IContentService _contentService;
private readonly ICoreScopeProvider _scopeProvider;
public async Task UpdateContentAsync(Guid key)
{
using var scope = _scopeProvider.CreateCoreScope();
IContent? content = _contentService.GetById(key);
if (content == null)
return;
// Modify content
content.SetValue("propertyAlias", "new value");
// Save (triggers notifications)
var result = _contentService.Save(content);
scope.Complete();
}
}5. Extending Content Types
public class ContentTypeCustomizationComposer : IComposer
{
public void Compose(IUmbracoBuilder builder)
{
builder.AddNotificationHandler<ContentTypeSavedNotification, MyHandler>();
}
}
public class MyHandler : INotificationHandler<ContentTypeSavedNotification>
{
public void Handle(ContentTypeSavedNotification notification)
{
foreach (var contentType in notification.SavedEntities)
{
// React to content type changes
}
}
}6. Defining Configuration
// 1. Create settings class in Configuration/Models/
public class MySettings
{
public string ApiKey { get; set; } = string.Empty;
public int MaxItems { get; set; } = 10;
}
// 2. Bind in Composer
builder.Services.Configure<MySettings>(
builder.Config.GetSection("Umbraco:MySettings"));
// 3. Inject via IOptionsMonitor<MySettings>Testing Considerations
The project has InternalsVisibleTo attributes for:
Umbraco.TestsUmbraco.Tests.CommonUmbraco.Tests.UnitTestsUmbraco.Tests.IntegrationUmbraco.Tests.Benchmarks
Internal types are accessible in test projects for more thorough testing.
Architecture Principles
- Separation of Concerns: Core defines contracts, Infrastructure implements them
- Interface-First: Always define interfaces before implementations
- Notification Pattern: Use notifications instead of events for extensibility
- Attempt Pattern: Return typed results with operation status
- Composer Pattern: Use composers for DI registration and configuration
- Scoping: Use scopes for unit of work and transaction management
- No Direct Database Access: Core has no repositories implementations
- Culture Variance: Full support for multi-language content
- Extensibility: Everything is designed to be extended or replaced
Common Gotchas
- Don't implement repositories in Core - They belong in Infrastructure
- Always use scopes - Database operations require a scope
- Complete scopes - Forgot
scope.Complete()? Changes won't save - Notification timing - *Saving notifications are cancellable, *Saved are not
- Service locator - Don't use
IServiceProviderdirectly, use DI - Culture handling - Many operations require explicit culture parameter
- Published vs Draft -
IContentis draft,IPublishedContentis published - Constants - Use constants instead of magic strings (property editor aliases, etc.)
- Distributed-cache-only publishers skip plain handlers - handlers doing durable work (e.g. DB writes) must implement
IDistributedCacheNotificationHandler/IDistributedCacheAsyncNotificationHandler<T>, or they won't fire under Umbraco Deploy and similar restricted scopes. - Outbound HTTP - new code injects
IHttpClientFactoryand uses a named client fromAddHttpClients();SharedHttpClientis only a shim for call sites that can't take that dependency without a breaking change. Never publish anHttpClientbefore configuring it, or mutate a shared one'sDefaultRequestHeadersafter first use -HttpHeadersisn't thread safe.
Navigation Tips
- Finding a service: Look in
/Servicesfor interfaces - Finding models: Check
/Modelsand subdirectories by domain - Finding notifications: Browse
/Notificationsfor available events - Finding configuration: Check
/Configuration/Models - Finding constants: Search
Constants-*.csfiles - Understanding operation results: Check
/Services/OperationStatus
Further Resources
- Implementation details are in Umbraco.Infrastructure
- Web functionality is in Umbraco.Web.Common
- API endpoints are in Umbraco.Cms.Api.* projects
- Official docs: https://docs.umbraco.com/
Remember: Umbraco.Core defines the contracts. Domain service implementations mostly live here too (Services/); repository and other Infrastructure-backed implementations live in Umbraco.Infrastructure. See root CLAUDE.md §5.
File: src/Umbraco.Examine.Lucene/CLAUDE.md
Umbraco.Examine.Lucene
Full-text search provider for Umbraco CMS using Examine and Lucene.NET. Provides content, media, member, and Delivery API indexing with configurable directory factories and index diagnostics.
Project Type: Class Library (NuGet package)
Target Framework: net10.0
Package ID: Umbraco.Cms.Examine.Lucene
Namespace: Umbraco.Cms.Infrastructure.Examine
Dependencies: Umbraco.Infrastructure, Examine (Lucene.NET wrapper)
1. Architecture
Project Purpose
This project provides Lucene-based full-text search capabilities for Umbraco:
- Index Types - Content, Media, Member, and Delivery API indexes
- Directory Factories - Configurable index storage (filesystem, temp, synced)
- Index Diagnostics - Health checks and metadata for backoffice
- Backoffice Search - Unified search across content tree with permissions
Folder Structure
Umbraco.Examine.Lucene/
├── DependencyInjection/
│ ├── ConfigureIndexOptions.cs # Per-index configuration (77 lines)
│ └── UmbracoBuilderExtensions.cs # AddExamineIndexes() registration (64 lines)
├── Extensions/
│ └── ExamineExtensions.cs # Lucene query parsing, health check
├── AddExamineComposer.cs # Auto-registration via IComposer
├── BackOfficeExamineSearcher.cs # Unified backoffice search (532 lines)
├── ConfigurationEnabledDirectoryFactory.cs # Directory factory selector
├── DeliveryApiContentIndex.cs # Headless API index with culture support
├── LuceneIndexDiagnostics.cs # Base diagnostics implementation
├── LuceneIndexDiagnosticsFactory.cs # Diagnostics factory
├── LuceneRAMDirectoryFactory.cs # In-memory directory for testing
├── NoPrefixSimpleFsLockFactory.cs # Lock factory without prefix
├── UmbracoApplicationRoot.cs # Application root path provider
├── UmbracoContentIndex.cs # Content/media index (158 lines)
├── UmbracoExamineIndex.cs # Base index class (153 lines)
├── UmbracoExamineIndexDiagnostics.cs # Extended diagnostics
├── UmbracoLockFactory.cs # Lock factory wrapper
├── UmbracoMemberIndex.cs # Member index
└── UmbracoTempEnvFileSystemDirectoryFactory.cs # Temp directory factoryIndex Hierarchy
LuceneIndex (Examine)
└── UmbracoExamineIndex (base for all Umbraco indexes)
├── UmbracoContentIndex (content + media)
├── UmbracoMemberIndex (members)
└── DeliveryApiContentIndex (headless API)2. Commands
For Git workflow and build commands, see repository root.
3. Key Components
Built-in Indexes (Constants.UmbracoIndexes)
| Index Name | Type | Purpose |
|---|---|---|
InternalIndex |
UmbracoContentIndex | Backoffice search (all content) |
ExternalIndex |
UmbracoContentIndex | Frontend published content |
MembersIndex |
UmbracoMemberIndex | Member search |
DeliveryApiContentIndex |
DeliveryApiContentIndex | Headless API content |
Note: See ConfigureIndexOptions for per-index analyzer and validator configuration.
UmbracoExamineIndex (UmbracoExamineIndex.cs)
Base class for all Umbraco indexes. Key features:
Runtime State Check (lines 84-95):
protected bool CanInitialize()
{
var canInit = _runtimeState.Level == RuntimeLevel.Run;
// Logs warning once if runtime not ready
}Prevents indexing during install/upgrade.
Special Field Transformations (lines 130-152):
__Pathfield - Enables descendant queries via path matching__Iconfield - Preserves icon for display
Raw Field Storage (lines 101-118):
Fields prefixed with __Raw_ are stored as StoredField (not analyzed), used for returning exact values.
UmbracoContentIndex (UmbracoContentIndex.cs)
Handles content and media indexing with validation and cascade deletes.
Cascade Delete (lines 128-157):
When content deleted, automatically finds and removes all descendants:
var descendantPath = $@"\-1\,*{nodeId}\,*";
var rawQuery = $"{UmbracoExamineFieldNames.IndexPathFieldName}:{descendantPath}";Validation Groups (lines 66-116):
Valid- Index normallyFailed- Skip (invalid data)Filtered- Delete from index (moved to recycle bin)
DeliveryApiContentIndex (DeliveryApiContentIndex.cs)
Specialized index for Delivery API with culture support.
Key Differences (lines 20-34):
ApplySpecialValueTransformations = false- No path/icon transformationsPublishedValuesOnly = false- Handles via populatorEnableDefaultEventHandler = false- Custom event handling
Composite IDs (lines 118-128):
Index IDs can be composite: "1234|da-DK" (content ID + culture) or simple: "1234".
BackOfficeExamineSearcher (BackOfficeExamineSearcher.cs)
Unified search for backoffice tree with user permissions.
Search Features:
- Node name boosting (10x for exact match, line 316-327)
- Wildcard support for partial matches (line 350-376)
- User start node filtering (lines 378-456)
- Recycle bin filtering (lines 185-191)
- Multi-language variant search (all
nodeName_{lang}fields)
Entity Type Routing (lines 89-155): Member→MembersIndex, Media→InternalIndex, Document→InternalIndex
4. Directory Factories
Configuration Options (IndexCreatorSettings.LuceneDirectoryFactory)
Default- FileSystemDirectoryFactory (standard filesystem storage)TempFileSystemDirectoryFactory- UmbracoTempEnvFileSystemDirectoryFactory (store in%TEMP%/ExamineIndexes)SyncedTempFileSystemDirectoryFactory- SyncedFileSystemDirectoryFactory (temp with sync to persistent)
UmbracoTempEnvFileSystemDirectoryFactory (lines 20-34)
Creates unique temp path using site name + application identifier hash:
var hashString = hostingEnvironment.SiteName + "::" + applicationIdentifier.GetApplicationUniqueIdentifier();
var cachePath = Path.Combine(Path.GetTempPath(), "ExamineIndexes", appDomainHash);Purpose: Prevents index collisions when same app moves between workers in load-balanced scenarios.
ConfigurationEnabledDirectoryFactory (lines 34-62)
Selector that creates appropriate factory based on IndexCreatorSettings.LuceneDirectoryFactory config value.
5. Index Configuration
ConfigureIndexOptions (DependencyInjection/ConfigureIndexOptions.cs)
Configures per-index options via IConfigureNamedOptions<LuceneDirectoryIndexOptions>.
Per-Index Settings (lines 39-61):
| Index | Analyzer | Validator |
|---|---|---|
| InternalIndex | CultureInvariantWhitespaceAnalyzer | ContentValueSetValidator |
| ExternalIndex | StandardAnalyzer | PublishedContentValueSetValidator |
| MembersIndex | CultureInvariantWhitespaceAnalyzer | MemberValueSetValidator |
| DeliveryApiContentIndex | StandardAnalyzer | None (populator handles) |
Global Settings (lines 63-70):
UnlockIndex = true- Always unlock on startup- Snapshot deletion policy when using SyncedTempFileSystemDirectoryFactory
6. Index Diagnostics
LuceneIndexDiagnostics (LuceneIndexDiagnostics.cs)
Provides health checks and metadata for backoffice examine dashboard.
Health Check (lines 41-45):
public Attempt<string?> IsHealthy()
{
var isHealthy = Index.IsHealthy(out Exception? indexError);
return isHealthy ? Attempt<string?>.Succeed() : Attempt.Fail(indexError?.Message);
}Metadata (lines 51-92):
- CommitCount, DefaultAnalyzer, LuceneDirectory type
- LuceneIndexFolder (relative path)
- DirectoryFactory and IndexDeletionPolicy types
UmbracoExamineIndexDiagnostics (UmbracoExamineIndexDiagnostics.cs)
Extends base with Umbraco-specific metadata (lines 23-48):
- EnableDefaultEventHandler
- PublishedValuesOnly
- Validator settings (IncludeItemTypes, ExcludeItemTypes, etc.)
7. Project-Specific Notes
Auto-Registration
AddExamineComposer (lines 10-14) automatically registers all Examine services:
builder
.AddExamine()
.AddExamineIndexes();Service Registrations (UmbracoBuilderExtensions.cs)
Key services registered (lines 18-62): IBackOfficeExamineSearcher, IIndexDiagnosticsFactory, IApplicationRoot, ILockFactory, plus all 4 indexes with ConfigurationEnabledDirectoryFactory.
Known Technical Debt
Warning Suppression (
.csproj:10-14):CS0618- Uses obsolete members in Examine/Lucene.NETTODO: Raw Query Support (
BackOfficeExamineSearcher.cs:71-76):csharp// TODO: WE should try to allow passing in a lucene raw query, however we will still need to do some manual string // manipulation for things like start paths, member types, etc...TODO: Query Parsing (
ExamineExtensions.cs:21-22):csharp// TODO: I'd assume there would be a more strict way to parse the query but not that i can find yet
Index Field Names
Key fields from UmbracoExamineFieldNames (defined in Umbraco.Core):
__Path- Content path for descendant queries__Icon- Content icon__Raw_*- Raw stored values (not analyzed)__Key- Content GUID__NodeTypeAlias- Content type alias__IndexType-content,media, ormember
Lucene Query Escaping
BackOfficeExamineSearcher.BuildQuery() (lines 193-311) handles:
- Special characters
*,-,_removal/replacement - Quoted phrase search (
"exact match") - Query escaping via
QueryParserBase.Escape() - Path escaping (replace
-with\-,,with\,)
User Start Node Filtering
BackOfficeExamineSearcher.AppendPath() (lines 378-456) ensures users only see content they have access to:
- Gets user's start nodes from
IBackOfficeSecurityAccessor - Filters search results to paths under start nodes
- Returns empty results if searchFrom outside user's access
Quick Reference
Index Names (Constants.UmbracoIndexes):
InternalIndexName= "InternalIndex"ExternalIndexName= "ExternalIndex"MembersIndexName= "MembersIndex"DeliveryApiContentIndexName= "DeliveryApiContentIndex"
Key Interfaces:
IUmbracoIndex- Marker for Umbraco indexesIUmbracoContentIndex- Content index markerIBackOfficeExamineSearcher- Backoffice search serviceIIndexDiagnostics- Health/metadata for dashboard
Dependencies:
Umbraco.Infrastructure- Core services, index populatorsUmbraco.Core- Field names, constants, interfacesExamine(NuGet) - Lucene.NET abstraction