docs: Add CLAUDE.md documentation for key .NET projects (#20841)

* docs: Add CLAUDE.md documentation for key .NET projects

Add comprehensive CLAUDE.md files for major Umbraco projects:
- Root CLAUDE.md: Multi-project repository overview
- Umbraco.Core: Interface contracts and domain models
- Umbraco.Infrastructure: Implementation layer (NPoco, migrations, services)
- Umbraco.Cms.Api.Common: Shared API infrastructure
- Umbraco.Cms.Api.Management: Management API (1,317 files, 54 domains)
- Umbraco.Web.UI.Client: Frontend with split docs structure

Each file includes:
- Architecture and design patterns
- Project-specific workflows
- Edge cases and gotchas
- Commands and setup
- Technical debt tracking

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Update src/Umbraco.Cms.Api.Management/CLAUDE.md

Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Andy Butland <abutland73@gmail.com>
Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>

* docs: Update CLAUDE.md with accurate persistence and auth info

- Clarify NPoco is current and fully supported (not legacy)
- Document EF Core as future direction with ongoing migration
- Add secure cookie-based token storage details for v17+
- Update OpenIddict authentication documentation
- Update API versioning (v1.0 and v1.1)
- Minor documentation cleanups (community links, descriptions)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Update src/Umbraco.Web.UI.Client/docs/agentic-workflow.md

* Update src/Umbraco.Web.UI.Client/docs/agentic-workflow.md

* Apply suggestions from code review

* Clarifications and duplicate removal.

---------

Co-authored-by: Phil Whittaker <pjw@umbraco.dk>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Co-authored-by: Andy Butland <abutland73@gmail.com>
Co-authored-by: Sven Geusens <geusens@gmail.com>
This commit is contained in:
hifi-phil
2025-11-25 14:37:49 +00:00
committed by GitHub
parent 1240d845d4
commit ef282a5211
15 changed files with 5537 additions and 0 deletions

336
CLAUDE.md Normal file
View File

@@ -0,0 +1,336 @@
# 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:
1. **Core Architecture** (Domain & Infrastructure)
- `Umbraco.Core` - Interface contracts, domain models, notifications
- `Umbraco.Infrastructure` - Service implementations, data access, caching
2. **Web & APIs** (Presentation Layer)
- `Umbraco.Web.UI` - Main ASP.NET Core web application
- `Umbraco.Web.Common` - Shared web functionality, controllers, middleware
- `Umbraco.Cms.Api.Management` - Backoffice Management API (REST)
- `Umbraco.Cms.Api.Delivery` - Content Delivery API (headless)
- `Umbraco.Cms.Api.Common` - Shared API infrastructure
3. **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.ImageSharp` v1 & v2 (image processing)
- Other: Static assets, targets, development tools
**6 Test Projects**:
- `Umbraco.Tests.Common` - Shared test utilities
- `Umbraco.Tests.UnitTests` - Unit tests
- `Umbraco.Tests.Integration` - Integration tests
- `Umbraco.Tests.Benchmarks` - Performance benchmarks
- `Umbraco.Tests.AcceptanceTest` - E2E tests
- `Umbraco.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
- **Swashbuckle** - OpenAPI/Swagger documentation
- **Lucene.NET** - Full-text search via Examine
- **ImageSharp** - Image processing
---
## 2. 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 analyzers
```
### Architecture Layers
**Dependency Flow** (unidirectional, always flows inward):
```
Web.UI → Web.Common → Infrastructure → Core
Api.Management → Api.Common → Infrastructure → Core
Api.Delivery → Api.Common → Infrastructure → Core
```
**Key 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.Core`
- `Umbraco.PublishedCache.*``Umbraco.Infrastructure`
- `Umbraco.Examine.Lucene``Umbraco.Infrastructure`
- `Umbraco.Cms.Persistence.*``Umbraco.Infrastructure`
**Web Layer**:
- `Umbraco.Web.Common``Umbraco.Infrastructure` + caching + search
- `Umbraco.Web.UI``Umbraco.Web.Common` + all features
**API Layer**:
- `Umbraco.Cms.Api.Common``Umbraco.Web.Common`
- `Umbraco.Cms.Api.Management``Umbraco.Cms.Api.Common`
- `Umbraco.Cms.Api.Delivery``Umbraco.Cms.Api.Common`
---
## 3. Teamwork & Collaboration
### Branching Strategy
- **Main branch**: `main` (protected)
- **Branch naming**:
- See `.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
### 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 documentation
```
### Code Owners
Project ownership is distributed across teams. Check individual project directories for ownership.
---
## 4. Architecture Patterns
### Core Architectural Decisions
1. **Layered Architecture with Dependency Inversion**
- Core defines contracts (interfaces)
- Infrastructure implements contracts
- Web/APIs consume implementations via DI
2. **Interface-First Design**
- All services defined as interfaces in Core
- Enables testing, polymorphism, extensibility
3. **Notification Pattern** (not C# events)
- See `/src/Umbraco.Core/CLAUDE.md` → "2. Notification System (Event Handling)"
4. **Composer Pattern** (DI registration)
- See `/src/Umbraco.Core/CLAUDE.md` → "3. Composer Pattern (DI Registration)"
5. **Scoping Pattern** (Unit of Work)
- See `/src/Umbraco.Core/CLAUDE.md` → "5. Scoping Pattern (Unit of Work)"
6. **Attempt Pattern** (operation results)
- `Attempt<TResult, TStatus>` instead of exceptions
- Strongly-typed operation status enums
### Key Design Patterns Used
- **Repository Pattern** - Data access abstraction
- **Unit of Work** - Scoping for transactions
- **Builder Pattern** - `ProblemDetailsBuilder` for 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
---
## 5. Project-Specific Notes
### Centralized Package Management
**All NuGet package versions** are centralized in `Directory.Packages.props`. Individual projects do NOT specify versions.
```xml
<!-- Individual projects reference WITHOUT version -->
<PackageReference Include="Swashbuckle.AspNetCore" />
<!-- Versions defined in Directory.Packages.props -->
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.5.0" />
```
### Build Configuration
- `Directory.Build.props` - Shared properties (target framework, company, copyright)
- `.editorconfig` - Code style rules
- `.globalconfig` - Roslyn analyzer rules
### 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
- ASP.NET Core Data Protection for token encryption
- Configured in `Umbraco.Cms.Api.Common`
- API requests must include credentials (`credentials: include` for fetch)
**Load Balancing Requirement**: All servers must share the same Data Protection key ring.
### 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/Swagger docs per version
### Known Limitations
1. **Circular Dependencies**: Avoided via `Lazy<T>` or event notifications
2. **Multi-Server**: Requires shared Data Protection key ring and synchronized clocks (NTP)
3. **Database Support**: SQL Server, SQLite
---
## Quick Reference
### Essential Commands
```bash
# 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 Release
```
### 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
### 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.**

View File

@@ -0,0 +1,382 @@
# 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
- **Swashbuckle** - OpenAPI/Swagger documentation generation
- **OpenIddict** - OAuth 2.0/OpenID Connect authentication
- **Asp.Versioning** - API versioning
- **System.Text.Json** - Polymorphic JSON serialization
### Dependencies
- `Umbraco.Core` - Domain models and service contracts
- `Umbraco.Web.Common` - Web functionality
### Project Structure (45 files)
```
Umbraco.Cms.Api.Common/
├── OpenApi/ # Schema/Operation ID handlers for Swagger
│ ├── SchemaIdHandler.cs # Generates schema IDs (e.g., "PagedUserModel")
│ ├── OperationIdHandler.cs # Generates operation IDs
│ └── SubTypesHandler.cs # Polymorphism support
├── Serialization/ # JSON type resolution
│ └── UmbracoJsonTypeInfoResolver.cs
├── Configuration/ # Options configuration
│ ├── ConfigureUmbracoSwaggerGenOptions.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 handlers
```
### Design Patterns
1. **Strategy Pattern** - `ISchemaIdHandler`, `IOperationIdHandler` (extensible via inheritance)
2. **Builder Pattern** - `ProblemDetailsBuilder` for fluent error responses
3. **Options Pattern** - All configuration via `IConfigureOptions<T>`
---
## 2. Commands
```bash
# Build
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
# Run tests (integration tests in consuming APIs)
dotnet test tests/Umbraco.Tests.Integration/
# Check for outdated/vulnerable 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 --vulnerable
```
---
## 3. Key Patterns
### Virtual Handlers for Extensibility
Handlers are intentionally virtual to allow consuming APIs to override:
```csharp
// NOTE: Left unsealed on purpose, so it is extendable.
public class SchemaIdHandler : ISchemaIdHandler
{
public virtual bool CanHandle(Type type) { }
public virtual string Handle(Type type) { }
}
```
**Why**: Management and Delivery APIs can customize schema/operation ID generation.
### Schema ID Sanitization (OpenApi/SchemaIdHandler.cs:32)
```csharp
// Remove invalid characters to prevent OpenAPI generation errors
return Regex.Replace(name, @"[^\w]", string.Empty);
// Add "Model" suffix to avoid TypeScript name clashes (line 24)
if (name.EndsWith("Model") == false)
{
name = $"{name}Model";
}
```
### Polymorphic Deserialization (Serialization/UmbracoJsonTypeInfoResolver.cs:31-34)
```csharp
// 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**:
```bash
# Run integration tests that exercise this library
dotnet test tests/Umbraco.Tests.Integration/
# Verify OpenAPI generation
# 1. Run Management API
# 2. Navigate to /umbraco/swagger/
# 3. Check schema IDs and operation IDs
```
**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 73-74):
```csharp
options
.UseReferenceAccessTokens()
.UseReferenceRefreshTokens();
```
**Why**: More secure (revocable), better for load balancing, uses ASP.NET Core Data Protection.
**Token Lifetime** (line 84-85):
```csharp
// Access token: 25% of refresh token lifetime
options.SetAccessTokenLifetime(new TimeSpan(timeOut.Ticks / 4));
options.SetRefreshTokenLifetime(timeOut);
```
**PKCE Required** (line 54-56):
```csharp
options
.AllowAuthorizationCodeFlow()
.RequireProofKeyForCodeExchange();
```
**Endpoints**:
- Backoffice: `/umbraco/management/api/v1/security/*`
- Member: `/umbraco/member/api/v1/security/*`
---
## 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):
```csharp
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));
}
```
**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** (OpenApi/SchemaIdHandler.cs:24-29):
```csharp
if (name.EndsWith("Model") == false)
{
// Add "Model" postfix to all models
name = $"{name}Model";
}
```
### Generic Type Handling
**Issue**: `PagedViewModel<T>` needs flattened schema name.
**Solution** (OpenApi/SchemaIdHandler.cs:41-49):
```csharp
// Turns "PagedViewModel<RelationItemViewModel>" into "PagedRelationItemModel"
return $"{name}{string.Join(string.Empty, type.GenericTypeArguments.Select(SanitizedTypeName))}";
```
---
## 7. Extending This Library
### Adding a Custom OpenAPI Handler
1. **Implement interface**:
```csharp
public class MySchemaIdHandler : SchemaIdHandler
{
public override bool CanHandle(Type type)
=> type.Namespace?.StartsWith("MyProject") is true;
public override string Handle(Type type)
=> $"My{base.Handle(type)}";
}
```
2. **Register in consuming API**:
```csharp
builder.Services.AddSingleton<ISchemaIdHandler, MySchemaIdHandler>();
```
**Note**: Handlers registered later take precedence in the selector.
### Customizing Problem Details
```csharp
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
### Why Reference Tokens Instead of JWT?
**Decision**: Use `UseReferenceAccessTokens()` and ASP.NET Core Data Protection.
**Tradeoffs**:
- ✅ **Pros**: Revocable, simpler key management, better security
- ❌ **Cons**: Requires database lookup (slower than JWT), needs shared Data Protection key ring
**Load Balancing Requirement**: All servers must share the same Data Protection key ring and application name.
### Why Virtual Handlers?
**Decision**: Make `SchemaIdHandler`, `OperationIdHandler`, etc. virtual.
**Why**: Management API and Delivery API have different schema ID requirements. Virtual methods allow override without rewriting the entire handler.
**Example**: Management API might prefix all schemas with "Management", Delivery API with "Delivery".
### Performance: Subtype Caching
**Implementation** (Serialization/UmbracoJsonTypeInfoResolver.cs:14):
```csharp
private readonly ConcurrentDictionary<Type, ISet<Type>> _subTypesCache = new();
```
**Why**: Reflection is expensive. Cache discovered subtypes to avoid repeated `ITypeFinder.FindClassesOfType()` calls.
### Known Limitations
1. **Polymorphic Deserialization**:
- Requires `$type` discriminator in JSON for interfaces
- Only discovers types in Umbraco namespaces
- Not all .NET types are discoverable
2. **OpenAPI Schema Generation**:
- Generic types are flattened (e.g., `PagedViewModel<T>` → `PagedTModel`)
- Type names may need "Model" suffix to avoid clashes
3. **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
**Swashbuckle**:
- OpenAPI 3.0 document generation
- Custom filters: `EnumSchemaFilter`, `MimeTypeDocumentFilter`, `RemoveSecuritySchemesDocumentFilter`
**Asp.Versioning**:
- API versioning via `ApiVersion` attribute
- API explorer integration for multi-version Swagger docs
### Configuration
**HTTPS** (Configuration/ConfigureOpenIddict.cs:14):
```csharp
// Disable transport security requirement for local development
options.DisableTransportSecurityRequirement = _globalSettings.Value.UseHttps is false;
```
**⚠️ Warning**: Never disable HTTPS in production.
### Usage by Consuming APIs
**Registration Pattern**:
```csharp
// In Umbraco.Cms.Api.Management or Umbraco.Cms.Api.Delivery
builder
.AddUmbracoApiOpenApiUI() // Swagger + custom handlers
.AddUmbracoOpenIddict(); // OAuth 2.0 authentication
```
---
## Quick Reference
### Essential Commands
```bash
# Build
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/
```
### Key Classes
| Class | Purpose | File |
|-------|---------|------|
| `ProblemDetailsBuilder` | Build RFC 7807 error responses | Builders/ProblemDetailsBuilder.cs |
| `SchemaIdHandler` | Generate OpenAPI schema IDs | OpenApi/SchemaIdHandler.cs |
| `UmbracoJsonTypeInfoResolver` | Polymorphic JSON serialization | Serialization/UmbracoJsonTypeInfoResolver.cs |
| `UmbracoBuilderAuthExtensions` | Configure OpenIddict | DependencyInjection/UmbracoBuilderAuthExtensions.cs |
| `PagedViewModel<T>` | Generic pagination model | ViewModels/Pagination/PagedViewModel.cs |
### Important Files
- `Umbraco.Cms.Api.Common.csproj` - Project dependencies
- `DependencyInjection/UmbracoBuilderApiExtensions.cs` - OpenAPI registration (line 12-30)
- `DependencyInjection/UmbracoBuilderAuthExtensions.cs` - OpenIddict setup (line 19-144)
- `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.**

View File

@@ -0,0 +1,621 @@
# 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 currently)
- **OpenAPI**: Swashbuckle.AspNetCore with custom schema/operation filters
- **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**: JsonPatch.Net for PATCH operations
- **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/ # Swashbuckle filters (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
- **JsonPatch.Net** - JSON Patch (RFC 6902) support
- **Swashbuckle.AspNetCore** - OpenAPI generation
### Design Patterns
1. **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)
2. **Presentation Factory Pattern** - Factories convert domain models to ViewModels
- Example: `IDocumentEditingPresentationFactory` (src/Umbraco.Cms.Api.Management/Factories/)
- Separation: Controllers → Factories → ViewModels
3. **Attempt Pattern** - Operations return `Attempt<TResult, TStatus>` for status-based error handling
- Controllers map status enums to HTTP status codes via helper methods
4. **Authorization Service Pattern** - All authorization via `IAuthorizationService`, not attributes
- Checked in base controller methods (see `ManagementApiControllerBase`)
5. **Options Pattern** - All configuration via `IOptions<T>` (security, routing, OpenAPI)
6. **SignalR Event Broadcasting** - Real-time notifications via `BackofficeHub` and `ServerEventHub`
---
## 2. Commands
### Build & Run
```bash
# 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 Release
```
### Code Quality
```bash
# 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=true
```
### OpenAPI Documentation
The project embeds a pre-generated `OpenApi.json` (1.3MB). To regenerate:
```bash
# Run Umbraco.Web.UI, access /umbraco/swagger
# Export JSON from Swagger UI
```
### Package Management
```bash
# 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 --vulnerable
```
### Environment Setup
1. **Prerequisites**: .NET 10 SDK
2. **IDE**: Visual Studio 2022 or Rider (with .editorconfig support)
3. **Configuration**: Inherits from `Umbraco.Web.UI` appsettings (no app settings in this library)
---
## 3. Style Guide
### Project-Specific Patterns
**Controller Naming** (line examples from CreateDocumentController.cs:16):
```csharp
[ApiVersion("1.0")]
public class CreateDocumentController : CreateDocumentControllerBase
```
- Pattern: `{Verb}{Entity}Controller` (e.g., `CreateDocumentController`, `UpdateMediaController`)
- Base class: `{Verb}{Entity}ControllerBase` for shared logic
- **Critical**: One operation per controller (not one controller per resource)
**Async Naming** - All async methods use `Async` suffix consistently:
```csharp
await _contentEditingService.CreateAsync(model, CurrentUserKey(_backOfficeSecurityAccessor));
```
**Factory Pattern Usage** (line 44):
```csharp
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 header
- `ContentEditingOperationStatusResult(status)` - Maps status enum to ProblemDetails
- `CurrentUserKey(accessor)` - Gets current user from security context
**Authorization Pattern** (all controllers):
```csharp
private readonly IAuthorizationService _authorizationService;
// Check permissions in action, not via [Authorize] attribute
```
---
## 4. 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
```bash
# All Management API tests
dotnet test --filter "FullyQualifiedName~Management"
# Specific domain (e.g., Document controllers)
dotnet test --filter "FullyQualifiedName~Management.Controllers.Document"
```
### Testing Focus
1. **Controller logic** - Request validation, authorization checks, status code mapping
2. **Factories** - ViewModel ↔ Domain model conversion accuracy
3. **Authorization** - Policy enforcement for each operation
4. **OpenAPI schema** - Ensure Swagger generation doesn't break
### InternalsVisibleTo
Tests have access to internal types (see .csproj:44-52):
- `Umbraco.Tests.UnitTests`
- `Umbraco.Tests.Integration`
- `DynamicProxyGenAssembly2` (for Moq)
---
## 5. Error Handling
### Operation Status Pattern
Controllers use `Attempt<TResult, TStatus>` with strongly-typed status enums:
```csharp
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 ProblemDetails
```
**Status 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
1. **Authorization failures** - Logged by AuthorizationService
2. **Service operation failures** - Logged in Infrastructure layer services
3. **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** (Routing/BackofficeHub.cs:33):
- `BackofficeHub` - User notifications, cache refreshes
- `ServerEventHub` - Background job updates, health checks
- Routes: `/umbraco/backoffice-signalr`, `/umbraco/serverevent-signalr`
### Code Smells to Watch For
1. **Large factory classes** - Some factories have 1000+ lines (e.g., `UserGroupPresentationFactory.cs`)
- Consider splitting by operation
2. **Repeated authorization checks** - Each controller duplicates auth logic
- Already abstracted to base classes, but still verbose
3. **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`
```csharp
// 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
```csharp
// 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 permissions
- `SectionAccessHandler` - Backoffice section access
- `UserGroupPermissionHandler` - 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**:
```csharp
// 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
```bash
# Check vulnerable packages
dotnet list src/Umbraco.Cms.Api.Management package --vulnerable
```
### Security Anti-Patterns to Avoid
1. **Never bypass authorization checks** - All operations must authorize
2. **Never trust client validation** - Always validate on server
3. **Never expose stack traces** - ProblemDetails abstracts errors
4. **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**:
1. **Publishing with descendants** - Can timeout on large trees
- Use `PublishDocumentWithDescendantsController` with result polling
- See `Controllers/Document/PublishDocumentWithDescendantsResultController.cs`
2. **Recycle bin operations** - Items in recycle bin can't be published
- Check `IsTrashed` before publish operations
3. **Public access rules** - Affects authorization and routing
- See `Controllers/Document/CreatePublicAccessDocumentController.cs`
**Media Upload**:
1. **Large file uploads** - Request size limits in parent app
- Controllers accept multipart/form-data
- Temporary files cleaned by background job
2. **Media picker** - Can reference deleted media
- Validation in `Factories/` checks for orphaned references
**User Management**:
1. **External logins** - Auto-linking can create duplicate users if email mismatches
- See `Security/ExternalSignInAutoLinkOptions.cs`
2. **User groups** - Deleting user group doesn't delete users
- Users reassigned to default group
**Webhooks**:
1. **Webhook failures** - Failed webhooks retry with exponential backoff
- See `Controllers/Webhook/` for configuration
### 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.cs
```
**BackOfficeController** - External login TODO:
```
// src/Umbraco.Cms.Api.Management/Controllers/Security/BackOfficeController.cs
// TODO: Handle external logins properly
```
---
## 10. Agentic Workflow
### When to Add a New Endpoint
**Decision Points**:
1. Does the operation fit an existing controller domain? (Document, Media, User, etc.)
2. Is this a new CRUD operation or a custom action?
3. Does this require new authorization policies?
**Workflow**:
1. **Create Controller** in appropriate `Controllers/{Domain}/` folder
- Follow naming: `{Verb}{Entity}Controller`
- Inherit from domain-specific base controller or `ManagementApiControllerBase`
2. **Define ViewModels** in `ViewModels/{Domain}/`
- Request model (e.g., `CreateDocumentRequestModel`)
- Response model (if not reusing existing)
3. **Create or Update Factory** in `Factories/`
- Map request ViewModel → domain model
- Map domain result → response ViewModel
4. **Authorization** - Check required permissions in controller action
- Use `IAuthorizationService.AuthorizeAsync()`
5. **Service Layer** - Call Core services (from `Umbraco.Core.Services`)
- Handle `Attempt<>` results
- Map status to HTTP status codes
6. **OpenAPI Annotations**:
- `[ApiVersion("1.0")]`
- `[MapToApiVersion("1.0")]`
- `[ProducesResponseType(...)]` for all status codes
7. **Testing**:
- Unit test controller logic
- Integration test end-to-end flow
8. **Update OpenApi.json** (if needed for client generation)
### Quality Gates Before PR
1. All tests pass
2. Code formatted (`dotnet format`)
3. No new warnings (check suppressed warnings list in .csproj:23)
4. OpenAPI schema valid (run Swagger UI)
5. Authorization tested (unit + integration tests)
### Common Pitfalls
1. **Forgetting authorization checks** - Every operation must authorize
2. **Inconsistent status code mapping** - Use base controller helpers
3. **Large factory classes** - Split by operation if >500 lines
4. **Missing ProducesResponseType** - Breaks OpenAPI client generation
5. **Not handling Attempt failures** - Always check `result.Success`
---
## 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 `OpenAPIContractTest` to 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
1. **API Versioning**: Currently v1.0 and v1.1
- Future versions will require new controller classes or action methods
2. **Batch Operations**: Limited batch endpoint support
- Most operations are single-entity (create one document at a time)
3. **Real-time Limits**: SignalR hubs don't scale beyond single-server without Redis backplane
- Configure Redis for multi-server setups
4. **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)
1. **Warnings Suppressed** (Umbraco.Cms.Api.Management.csproj:9-22):
```
TODO: 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 cref
```
2. **Tree 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`
3. **External Login Handling** (Controllers/Security/BackOfficeController.cs):
- TODO: Handle external logins properly
4. **Large Factory Classes** (Factories/UserGroupPresentationFactory.cs):
- Some factories exceed 1000 lines - consider splitting
5. **ViewModel Explosion** - 1000+ ViewModel classes
- Consider shared base models or composition to reduce duplication
---
## Quick Reference
### Essential Commands
```bash
# 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 Release
```
### Key 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.Common` project
### 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 document
- `GET /umbraco/management/api/v1/document/{id}` - Get document by key
- `PUT /umbraco/management/api/v1/document/{id}` - Update document
- `DELETE /umbraco/management/api/v1/document/{id}` - Delete document
Full spec: See OpenApi.json or Swagger UI at `/umbraco/swagger`
### 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

519
src/Umbraco.Core/CLAUDE.md Normal file
View File

@@ -0,0 +1,519 @@
# 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 handlers
```
### Infrastructure & 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 abstractions
```
### Supporting 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 infrastructure
```
## Core Patterns and Conventions
### 1. Service Layer Pattern
Services follow a consistent structure:
```csharp
// Interface defines contract (in Umbraco.Core)
public interface IContentService
{
IContent? GetById(Guid key);
Task<Attempt<IContent?, ContentEditingOperationStatus>> CreateAsync(...);
}
// Implementation lives in Umbraco.Infrastructure
// Service operations return Attempt<T, TStatus> for typed results
```
**Key conventions**:
- Interfaces in Umbraco.Core, implementations in Umbraco.Infrastructure
- Use `Attempt<TResult, TStatus>` for 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:
```csharp
// 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 via `ICancelableNotification`)
- `*SavedNotification` - After save
- `*DeletingNotification` / `*DeletedNotification`
- `*MovingNotification` / `*MovedNotification`
**Key interface**: `IEventAggregator` - publishes notifications to handlers
### 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:
```csharp
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 - Members
```
**Key interfaces**:
- `IContentBase` - Common base for all content items (properties, cultures)
- `IContent` - Documents with publishing workflow
- `IContentType`, `IMediaType`, `IMemberType` - Define structure
- `IProperty` - Individual property on content
- `IPropertyType` - Property definition on content type
### 5. Scoping Pattern (Unit of Work)
```csharp
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)
- `RepositoryCacheMode` controls caching behavior
### 6. Property Editors
Property editors define how data is edited and stored:
```csharp
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 registration
- `IDataValueEditor` - Value editing and conversion
- `IPropertyIndexValueFactory` - Search indexing
- `IPropertyValueConverter` - Convert stored values to typed values
### 7. Cache Refreshers (Distributed Cache)
For multi-server deployments, cache refreshers synchronize cache:
```csharp
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 operations
- `IContentTypeService` - Content type management
- `IMediaService` - Media operations
- `IDataTypeService` - Data type configuration
- `IUserService` - User management
- `ILocalizationService` - Languages and dictionary
- `IRelationService` - Entity relationships
#### Content Models
- `IContent` / `IContentBase` - Document entities
- `IContentType` - Document type definition
- `IProperty` / `IPropertyType` - Property definitions
- `IPublishedContent` - Read-only published content
#### Infrastructure
- `ICoreScopeProvider` - Unit of work / transactions
- `IEventAggregator` - Notification publishing
- `IUmbracoMapper` - Object mapping
- `IComposer` / `IUmbracoBuilder` - DI registration
#### Entity Base Classes
- `EntityBase` - Basic entity with Id, Key, dates
- `TreeEntityBase` - Adds Name, hierarchy
- `BeingDirty` / `ICanBeDirty` - Change tracking
## Key Files and Constants
### Essential Files
1. **Constants.cs** (and 40 Constants-*.cs files)
- `Constants.System.*` - System-level constants
- `Constants.Security.*` - Security and authorization
- `Constants.Conventions.*` - Naming conventions
- `Constants.PropertyEditors.*` - Built-in property editor aliases
- `Constants.ObjectTypes.*` - Entity type GUIDs
- Use these instead of magic strings!
2. **Udi.cs** / **GuidUdi.cs** / **StringUdi.cs**
- Umbraco Identifiers (like URIs): `umb://document/{guid}`
- Used throughout for entity references
- `UdiParser.Parse("umb://document/...")` to parse
3. **Attempt.cs** and **Attempt<TResult, TStatus>.cs**
- Result pattern for operations that can fail
- `Attempt.Succeed(value)` / `Attempt.Fail<T>()`
- `Attempt<Content, ContentEditingOperationStatus>` - typed result with status
### Configuration
Configuration models in `/Configuration/Models`:
- `ContentSettings` - Content-related settings
- `GlobalSettings` - Global Umbraco settings
- `SecuritySettings` - Security configuration
- `DeliveryApiSettings` - Delivery API configuration
- Access via `IOptionsMonitor<TSettings>`
## 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
```csharp
// 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.Infrastructure
// 4. Register in a Composer
builder.Services.AddScoped<IMyService, MyService>();
```
### 2. Adding a Notification Handler
```csharp
// 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
```csharp
// 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
```csharp
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
```csharp
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
```csharp
// 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.Tests`
- `Umbraco.Tests.Common`
- `Umbraco.Tests.UnitTests`
- `Umbraco.Tests.Integration`
- `Umbraco.Tests.Benchmarks`
Internal types are accessible in test projects for more thorough testing.
## Architecture Principles
1. **Separation of Concerns**: Core defines contracts, Infrastructure implements them
2. **Interface-First**: Always define interfaces before implementations
3. **Notification Pattern**: Use notifications instead of events for extensibility
4. **Attempt Pattern**: Return typed results with operation status
5. **Composer Pattern**: Use composers for DI registration and configuration
6. **Scoping**: Use scopes for unit of work and transaction management
7. **No Direct Database Access**: Core has no repositories implementations
8. **Culture Variance**: Full support for multi-language content
9. **Extensibility**: Everything is designed to be extended or replaced
## Common Gotchas
1. **Don't implement repositories in Core** - They belong in Infrastructure
2. **Always use scopes** - Database operations require a scope
3. **Complete scopes** - Forgot `scope.Complete()`? Changes won't save
4. **Notification timing** - *Saving notifications are cancellable, *Saved are not
5. **Service locator** - Don't use `IServiceProvider` directly, use DI
6. **Culture handling** - Many operations require explicit culture parameter
7. **Published vs Draft** - `IContent` is draft, `IPublishedContent` is published
8. **Constants** - Use constants instead of magic strings (property editor aliases, etc.)
## Navigation Tips
- **Finding a service**: Look in `/Services` for interfaces
- **Finding models**: Check `/Models` and subdirectories by domain
- **Finding notifications**: Browse `/Notifications` for available events
- **Finding configuration**: Check `/Configuration/Models`
- **Finding constants**: Search `Constants-*.cs` files
- **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 is about **defining what**, not **implementing how**. Keep implementations in Infrastructure!

View File

@@ -0,0 +1,777 @@
# Umbraco CMS - Infrastructure
Implementation layer for Umbraco CMS, providing concrete implementations of all Core interfaces. Handles database access (NPoco), caching, background jobs, migrations, search indexing (Examine), email (MailKit), and logging (Serilog).
**Project**: `Umbraco.Infrastructure`
**Type**: .NET Library
**Files**: 1,006 C# files implementing Core contracts
---
## 1. Architecture
### Target Framework
- **.NET 10.0** (`net10.0`)
- **C# 12** with nullable reference types enabled
- **Library** (no executable)
### Application Type
**Infrastructure Layer** - Implements all interfaces defined in `Umbraco.Core`. This is where contracts meet concrete implementations.
### Key Technologies
- **Database Access**: NPoco (micro-ORM) with SQL Server & SQLite support
- **Caching**: In-memory + distributed cache via `IAppCache`
- **Background Jobs**: Recurring jobs via `IRecurringBackgroundJob` and `IDistributedBackgroundJob`
- **Search**: Examine (Lucene.NET wrapper) for full-text search
- **Email**: MailKit for SMTP email
- **Logging**: Serilog with structured logging
- **Migrations**: Custom migration framework for database schema + data
- **DI**: Microsoft.Extensions.DependencyInjection
- **Serialization**: System.Text.Json
- **Identity**: Microsoft.Extensions.Identity.Stores for user/member management
- **Authentication**: OpenIddict.Abstractions
### Project Structure
```
src/Umbraco.Infrastructure/
├── Persistence/ # Database access (NPoco)
│ ├── Repositories/ # Repository implementations (47 repos)
│ │ └── Implement/ # Concrete repository classes
│ ├── Dtos/ # Database DTOs (80+ files)
│ ├── Mappers/ # Entity ↔ DTO mappers (43 mappers)
│ ├── Factories/ # Entity factories (28 factories)
│ ├── Querying/ # Query builders and translators
│ ├── SqlSyntax/ # SQL dialect handlers (SQL Server, SQLite)
│ ├── DatabaseModelDefinitions/ # Table/column definitions
│ └── UmbracoDatabase.cs # Main database wrapper (NPoco)
├── Services/ # Service implementations
│ └── Implement/ # Concrete service classes (16 services)
│ ├── ContentService.cs # Content CRUD operations
│ ├── MediaService.cs # Media operations
│ ├── UserService.cs # User management
│ └── [13 more services...]
├── Scoping/ # Unit of Work implementation
│ ├── ScopeProvider.cs # Transaction/scope management
│ ├── Scope.cs # Unit of work implementation
│ └── AmbientScopeContextStack.cs # Async-safe scope context
├── Migrations/ # Database migration system
│ ├── Install/ # Initial database schema
│ ├── Upgrade/ # Version upgrade migrations (21 versions)
│ ├── PostMigrations/ # Post-upgrade data fixes
│ ├── MigrationPlan.cs # Migration orchestration
│ └── MigrationPlanExecutor.cs # Migration execution
├── BackgroundJobs/ # Background job infrastructure
│ ├── Jobs/ # Concrete job implementations
│ │ ├── ReportSiteJob.cs # Telemetry reporting
│ │ ├── TempFileCleanupJob.cs # Cleanup temp files
│ │ └── ServerRegistration/ # Multi-server coordination
│ └── RecurringBackgroundJobHostedService.cs # Job scheduler
├── Examine/ # Search indexing (Lucene)
│ ├── ContentValueSetBuilder.cs # Index document content
│ ├── MediaValueSetBuilder.cs # Index media
│ ├── MemberValueSetBuilder.cs # Index members
│ ├── DeliveryApiContentIndexPopulator.cs # Delivery API indexing
│ └── Deferred/ # Deferred index updates
├── Security/ # Identity & authentication
│ ├── BackOfficeUserStore.cs # User store (Identity)
│ ├── MemberUserStore.cs # Member store (Identity)
│ ├── BackOfficeIdentity*.cs # Identity configuration
│ └── Passwords/ # Password hashing
├── PropertyEditors/ # Property editor implementations (75 files)
│ ├── ValueConverters/ # Convert stored → typed values
│ ├── Validators/ # Property validation
│ ├── Configuration/ # Editor configuration
│ └── DeliveryApi/ # Delivery API converters
├── Cache/ # Cache implementation & invalidation
│ ├── DatabaseServerMessengerNotificationHandler.cs # Multi-server cache sync
│ └── PropertyEditors/ # Property editor caching
├── Serialization/ # JSON serialization (20 files)
│ ├── SystemTextJsonSerializer.cs # Main serializer
│ └── Converters/ # Custom JSON converters
├── Mail/ # Email (MailKit)
│ ├── EmailSender.cs # SMTP email sender
│ └── EmailMessageExtensions.cs
├── Logging/ # Serilog setup
│ ├── Serilog/ # Serilog enrichers
│ └── MessageTemplates.cs # Structured logging templates
├── Mapping/ # Object mapping (IUmbracoMapper)
│ └── UmbracoMapper.cs # AutoMapper-like functionality
├── Notifications/ # Notification handlers
├── Packaging/ # Package import/export
├── ModelsBuilder/ # Strongly-typed models generation
├── Routing/ # URL routing implementation
├── Runtime/ # Application lifecycle
├── Search/ # Search services
├── Sync/ # Multi-server synchronization
├── Telemetry/ # Analytics/telemetry
└── Templates/ # Template parsing
```
### Dependencies
- **Umbraco.Core** - All interface contracts (only dependency)
- **NPoco** - Micro-ORM for database access
- **Examine.Core** - Search indexing abstraction
- **MailKit** - Email sending
- **HtmlAgilityPack** - HTML parsing
- **Serilog** - Structured logging
- **ncrontab** - Cron expression parsing for background jobs
- **OpenIddict.Abstractions** - OAuth/OIDC abstractions
- **Microsoft.Extensions.Identity.Stores** - Identity storage
### Design Patterns
1. **Repository Pattern** - All database access via repositories
- Example: `UserRepository`, `ContentRepository`, `MediaRepository`
- Implements interfaces from Umbraco.Core
2. **Unit of Work** - Scoping pattern for transactions
- `IScopeProvider` / `Scope` - transaction management
- Must call `scope.Complete()` to commit
3. **Factory Pattern** - Entity factories convert DTOs → domain models
- Example: `ContentFactory`, `MediaFactory`, `UserFactory`
- Located in `Persistence/Factories/`
4. **Mapper Pattern** - Bidirectional DTO ↔ Entity mapping
- Example: `ContentMapper`, `MediaMapper`, `UserMapper`
- Located in `Persistence/Mappers/`
5. **Strategy Pattern** - SQL syntax providers for different databases
- `SqlServerSyntaxProvider`, `SqliteSyntaxProvider`
- Abstracted via `ISqlSyntaxProvider`
6. **Migration Pattern** - Version-based database migrations
- `MigrationPlan` defines migration graph
- `MigrationPlanExecutor` runs migrations in order
7. **Builder Pattern** - `ValueSetBuilder` for search indexing
---
## 2. Commands
### Build & Test
```bash
# Build
dotnet build src/Umbraco.Infrastructure
# Test (tests in ../../tests/)
dotnet test --filter "FullyQualifiedName~Infrastructure"
# Pack
dotnet pack src/Umbraco.Infrastructure -c Release
```
### Code Quality
```bash
# Format code
dotnet format src/Umbraco.Infrastructure
# Build with all warnings (note: many suppressed, see .csproj:31)
dotnet build src/Umbraco.Infrastructure /p:TreatWarningsAsErrors=true
```
### Database Migrations (Developer Context)
This project contains the migration framework but **migrations are NOT run via EF Core**. Migrations run at application startup via `MigrationPlanExecutor`.
To create a new migration:
1. Create class inheriting `MigrationBase` in `Migrations/Upgrade/`
2. Add to `UmbracoPlan` migration plan
3. Restart application - migration runs automatically
### Package Management
```bash
# Check for vulnerable packages
dotnet list src/Umbraco.Infrastructure package --vulnerable
# Check outdated packages (versions in Directory.Packages.props)
dotnet list src/Umbraco.Infrastructure package --outdated
```
### Environment Setup
1. **Prerequisites**: .NET 10 SDK, SQL Server or SQLite
2. **IDE**: Visual Studio 2022, Rider, or VS Code
3. **Database**: Automatically created on first run (see Install/DatabaseSchemaCreator.cs)
---
## 3. Style Guide
### Project-Specific Patterns
**Repository Naming** (from UserRepository.cs:30):
```csharp
internal sealed class UserRepository : EntityRepositoryBase<Guid, IUser>, IUserRepository
```
- Pattern: `{Entity}Repository : EntityRepositoryBase<TId, TEntity>, I{Entity}Repository`
- Always `internal sealed` (not exposed outside assembly)
- Inherit from `EntityRepositoryBase` for common CRUD
**Factory Naming** (consistent across Factories/ directory):
```csharp
internal class ContentFactory : IEntityFactory<IContent, ContentDto>
```
- Pattern: `{Entity}Factory : IEntityFactory<TEntity, TDto>`
- Converts DTO → Domain entity
- Always `internal` (implementation detail)
**Service Naming** (from Services/Implement/):
```csharp
internal sealed class ContentService : RepositoryService, IContentService
```
- Pattern: `{Domain}Service : RepositoryService, I{Domain}Service`
- Inherits `RepositoryService` for scope/repository access
- Always `internal sealed`
### Key Code Patterns
**Scope Usage** (required for all database operations):
```csharp
using (ICoreScope scope = ScopeProvider.CreateCoreScope())
{
// Database operations here
scope.Complete(); // MUST call to commit
}
```
**NPoco Query Building** (from repositories):
```csharp
Sql<ISqlContext> sql = Sql()
.Select<ContentDto>()
.From<ContentDto>()
.Where<ContentDto>(x => x.NodeId == id);
```
---
## 4. Test Bench
### Test Location
- **Unit Tests**: `tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/`
- **Integration Tests**: `tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/`
- **Benchmarks**: `tests/Umbraco.Tests.Benchmarks/`
### Running Tests
```bash
# All Infrastructure tests
dotnet test --filter "FullyQualifiedName~Infrastructure"
# Specific area (e.g., Persistence)
dotnet test --filter "FullyQualifiedName~Infrastructure.Persistence"
# Integration tests only
dotnet test --filter "Category=Integration&FullyQualifiedName~Infrastructure"
```
### Testing Focus
1. **Repository tests** - CRUD operations, query translation
2. **Scope tests** - Transaction behavior, nested scopes
3. **Migration tests** - Schema creation, upgrade paths
4. **Service tests** - Business logic, notification firing
5. **Mapper tests** - DTO ↔ Entity conversion accuracy
### InternalsVisibleTo
Tests have access to internal types (see .csproj:73-93):
- `Umbraco.Tests`, `Umbraco.Tests.UnitTests`, `Umbraco.Tests.Integration`, `Umbraco.Tests.Common`, `Umbraco.Tests.Benchmarks`
- `DynamicProxyGenAssembly2` (for Moq)
---
## 5. Error Handling
### Attempt Pattern (from Services)
Services return `Attempt<TResult, TStatus>` from Core:
```csharp
Attempt<IContent?, ContentEditingOperationStatus> result =
await _contentService.CreateAsync(model, userKey);
if (!result.Success)
{
// result.Status contains typed error (e.g., ContentEditingOperationStatus.NotFound)
}
```
### Database Error Handling
**NPoco Exception Wrapping**:
- `NPocoSqlException` wraps database errors
- Check `InnerException` for underlying DB error
- SQL syntax errors logged via `ILogger<T>`
### Scope Error Handling
**Critical**: If scope not completed, transaction rolls back:
```csharp
using (ICoreScope scope = ScopeProvider.CreateCoreScope())
{
try
{
// Operations
scope.Complete(); // MUST be called
}
catch
{
// Transaction automatically rolled back if not completed
throw;
}
}
```
### Critical Logging Points
1. **Migration failures** - Logged in `MigrationPlanExecutor.cs`
2. **Database connection errors** - Logged in `UmbracoDatabaseFactory.cs`
3. **Repository exceptions** - Logged in `EntityRepositoryBase`
4. **Background job failures** - Logged in `RecurringBackgroundJobHostedService.cs`
---
## 6. Clean Code
### Key Design Decisions
**Why NPoco instead of EF Core?**
- Performance: NPoco is faster for Umbraco's read-heavy workload
- Control: Fine-grained control over SQL generation
- Flexibility: Easier to optimize complex queries
- History: Legacy decision, but still valid (EF Core support added in parallel)
**Why Custom Migration Framework?**
- Pre-dates EF Core Migrations
- Supports data migrations + schema migrations
- Graph-based dependencies (not linear like EF Core)
- Can target multiple database providers with same migration
- Located in `Migrations/` directory
**Why EntityRepositoryBase?**
- DRY: Common CRUD operations (Get, GetMany, Save, Delete)
- Consistency: All repos follow same patterns
- Caching: Integrated cache invalidation
- Scoping: Automatic transaction management
**Why Separate DTOs?**
- Database schema != domain model
- DTOs are flat, entities have relationships
- Allows independent evolution of schema vs domain
- Located in `Persistence/Dtos/`
### Architectural Decisions
**Repository Layer** (Persistence/Repositories/Implement/):
- 47 repository implementations
- All inherit from `EntityRepositoryBase<TId, TEntity>`
- Repositories do NOT fire notifications (services do)
**Service Layer** (Services/Implement/):
- 16 service implementations
- Services fire notifications before/after operations
- Services manage scopes (not repositories)
- Example: `ContentService`, `MediaService`, `UserService`
### Code Smells to Watch For
1. **Forgetting `scope.Complete()`** - Transaction silently rolls back
2. **Nested scopes without awareness** - Only innermost scope controls commit
3. **Lazy loading outside scope** - NPoco relationships must load within scope
4. **Large migrations** - Split into multiple steps if > 1000 lines
5. **Repository logic in services** - Keep repos thin, logic in services
---
## 7. Security
### Input Validation
**At Service Layer**:
- Services validate before calling repositories
- FluentValidation NOT used (manual validation)
- Example: `ContentService` validates content type exists before creating content
### Data Access Security
**SQL Injection Prevention**:
- NPoco uses parameterized queries automatically
- Never concatenate SQL strings
- All queries via `Sql<ISqlContext>` builder:
```csharp
Sql().Select<ContentDto>().Where<ContentDto>(x => x.NodeId == id) // Safe
Database.Query<ContentDto>($"SELECT * FROM Content WHERE id = {id}") // NEVER do this
```
### Authentication & Authorization
**Identity Stores** (Security/):
- `BackOfficeUserStore.cs` - User store for ASP.NET Core Identity
- `MemberUserStore.cs` - Member store for ASP.NET Core Identity
- Password hashing via `IPasswordHasher<T>` (PBKDF2)
**Password Security** (Security/Passwords/):
- PBKDF2 with 10,000 iterations (configurable)
- Salted hashes stored in database
- Legacy hash formats supported for migration
### Secrets Management
**No secrets in this library** - Configuration from parent application (Umbraco.Web.UI):
- Connection strings from `appsettings.json`
- SMTP credentials from `appsettings.json`
- Email sender uses `IOptions<EmailSenderSettings>`
### Dependency Security
```bash
# Check vulnerable dependencies
dotnet list src/Umbraco.Infrastructure package --vulnerable
```
### Security Anti-Patterns to Avoid
1. **Raw SQL queries** - Always use NPoco `Sql<ISqlContext>` builder
2. **Storing plain text passwords** - Use Identity's password hasher
3. **Exposing internal types** - Keep repos/services `internal`
4. **Logging sensitive data** - Never log passwords, connection strings, API keys
---
## 8. Teamwork and Workflow
**⚠️ SKIPPED** - This is a sub-project. See root `/CLAUDE.md` for repository-wide teamwork protocols.
---
## 9. Edge Cases
### Scope Edge Cases
**Nested Scopes** - Only innermost scope commits:
```csharp
using (var outer = ScopeProvider.CreateCoreScope())
{
using (var inner = ScopeProvider.CreateCoreScope())
{
// Work
inner.Complete(); // This does nothing!
}
outer.Complete(); // This commits
}
```
**Async Scopes** - Scopes are NOT thread-safe:
- Don't pass scopes across threads
- Don't use scopes in Parallel.ForEach
- Create new scope on each async operation
**SQLite Lock Contention**:
- SQLite has database-level locking
- Multiple concurrent writes = lock errors
- Use `[SuppressMessage]` for known SQLite lock issues
- See `UserRepository.cs:39` - `_sqliteValidateSessionLock`
### Migration Edge Cases
**Migration Rollback** - NOT SUPPORTED:
- Migrations are one-way only
- Test migrations thoroughly before release
- Use database backups for rollback
**Migration Dependencies** - Graph-based:
- Migrations can have multiple dependencies
- Dependencies resolved via `MigrationPlan`
- Circular dependencies throw `InvalidOperationException`
**Data Migrations** - Can be slow:
- Migrations run at startup (blocking)
- Large data migrations (> 100k rows) should be chunked
- Use `AsyncMigrationBase` for long-running operations
### Repository Edge Cases
**Cache Invalidation**:
- Repository CRUD operations invalidate cache automatically
- Bulk operations may not invalidate correctly
- Repositories fire cache refreshers via `DistributedCache`
**NPoco Lazy Loading**:
- Relationships must be loaded within scope
- Accessing lazy-loaded properties outside scope throws
- Use `.FetchOneToMany()` to eager load
### Background Job Edge Cases
**Multi-Server Coordination**:
- Background jobs use server registration to coordinate
- Only "main" server runs jobs (determined by DB lock)
- If main server dies, another takes over within 5 minutes
---
## 10. Agentic Workflow
### When to Add a New Repository
**Decision Points**:
1. Does the entity have a Core interface in `Umbraco.Core/Persistence/Repositories`?
2. Is this entity persisted to the database?
3. Does it require custom queries beyond basic CRUD?
**Workflow**:
1. **Create DTO** in `Persistence/Dtos/` (matches database table)
2. **Create Mapper** in `Persistence/Mappers/` (DTO ↔ Entity)
3. **Create Factory** in `Persistence/Factories/` (DTO → Entity)
4. **Create Repository** in `Persistence/Repositories/Implement/`
- Inherit from `EntityRepositoryBase<TId, TEntity>`
- Implement interface from Core
5. **Register in Composer** (DependencyInjection/)
6. **Write Tests** (unit + integration)
### When to Add a New Service
**Decision Points**:
1. Does the service have a Core interface in `Umbraco.Core/Services`?
2. Does it coordinate multiple repositories?
3. Does it need to fire notifications?
**Workflow**:
1. **Implement Interface** from Core in `Services/Implement/`
- Inherit from `RepositoryService`
- Inject repositories via constructor
2. **Add Notification Firing**:
- Fire `*SavingNotification` before operation (cancellable)
- Fire `*SavedNotification` after operation
3. **Manage Scopes** - Services create scopes, not repositories
4. **Register in Composer**
5. **Write Tests**
### When to Add a Migration
**Decision Points**:
1. Is this a schema change (tables, columns, indexes)?
2. Is this a data migration (update existing data)?
3. Which version does this target?
**Workflow**:
1. **Create Migration Class** in `Migrations/Upgrade/V{Version}/`
- Inherit from `MigrationBase` (schema) or `AsyncMigrationBase` (data)
- Implement `Migrate()` method
2. **Add to UmbracoPlan** in `Migrations/Upgrade/UmbracoPlan.cs`
- Specify dependencies (runs after which migrations?)
3. **Test Migration**:
- Integration test with database
- Test upgrade from previous version
4. **Document Breaking Changes** (if any)
### Quality Gates Before PR
1. All tests pass
2. Code formatted (`dotnet format`)
3. No new warnings (check suppressed warnings list in .csproj:31)
4. Database migrations tested (upgrade from previous version)
5. Scope usage correct (all scopes completed)
### Common Pitfalls
1. **Forgetting `scope.Complete()`** - Transaction rolls back silently
2. **Repository logic in services** - Keep repos focused on data access
3. **Missing cache invalidation** - Repositories auto-invalidate, but custom queries may not
4. **Missing notifications** - Services must fire notifications
5. **Eager loading outside scope** - NPoco relationships must load within scope
6. **Large migrations** - Chunk data migrations for performance
---
## 11. Project-Specific Notes
### Key Design Decisions
**Why 1,006 files for "just" implementation?**
- 47 repositories × ~3 files each (repo, mapper, factory) = ~141 files
- 75 property editors × ~2 files each = ~150 files
- 80 DTOs for database tables = 80 files
- 21 versions × ~5 migrations each = ~105 files
- Remaining: services, background jobs, search, email, logging, etc.
**Why NPoco + Custom Migrations?**
- Historical: Predates EF Core
- Performance: Faster than EF Core for Umbraco's workload
- Control: Fine-grained SQL control
- **Note**: EF Core support added in parallel (`Umbraco.Cms.Persistence.EFCore` project)
**Why Separate Factories and Mappers?**
- **Factories**: DTO → Entity (one direction, for reading from DB)
- **Mappers**: DTO ↔ Entity (bidirectional, includes column mapping metadata)
- Factories use Mappers under the hood
### External Integrations
**Email (MailKit)**:
- SMTP email via `MailKit` library (version in `Directory.Packages.props`)
- Configured via `IOptions<EmailSenderSettings>`
- Supports TLS, SSL, authentication
**Search (Examine)**:
- Lucene.NET wrapper
- Indexes content, media, members
- `ValueSetBuilder` classes convert entities → search documents
- Located in `Examine/` directory
**Logging (Serilog)**:
- Structured logging throughout
- Enrichers: Process ID, Thread ID
- Sinks: File, Async
- Configuration in `appsettings.json` of parent app
**Background Jobs (Recurring)**:
- Cron-based scheduling via `ncrontab`
- `IRecurringBackgroundJob` interface
- Jobs: Telemetry, temp file cleanup, server registration
- Located in `BackgroundJobs/Jobs/`
### Known Limitations
1. **NPoco Lazy Loading** - Must load relationships within scope
2. **Migration Rollback** - One-way only, no rollback support
3. **SQLite Locking** - Database-level locks cause contention
4. **Single Database** - No multi-database support (e.g., read replicas)
5. **Background Jobs** - Single-server only (distributed jobs require additional setup)
### Performance Considerations
**Caching**:
- Repository results cached automatically
- Cache invalidation via `DistributedCache`
- Multi-server cache sync via database messenger
**Database Connection Pooling**:
- ADO.NET connection pooling enabled by default
- Configured in connection string
**N+1 Query Problem**:
- NPoco supports eager loading via `.FetchOneToMany()`
- Always profile queries in development
**Background Jobs**:
- Run on background threads (don't block web requests)
- Use `IRecurringBackgroundJob` for scheduled tasks
- Use `IDistributedBackgroundJob` for multi-server coordination
### Technical Debt (from TODO comments in .csproj and code)
1. **Warnings Suppressed** (Umbraco.Infrastructure.csproj:10-30):
```
TODO: Fix and remove overrides:
- CS0618: handle member obsolete appropriately
- CA1416: validate platform compatibility
- SA1117: params all on same line
- SA1401: make fields private
- SA1134: own line attributes
- CA2017: match parameters number
- CS0108: hidden inherited member
- SYSLIB0051: formatter-based serialization
- SA1649: filename match type name
- CS1998: remove async or make method synchronous
- CS0169: unused field
- CS0114: hidden inherited member
- IDE0060: remove unused parameter
- SA1130: use lambda syntax
- IDE1006: naming violation
- CS1066: default value
- CS0612: obsolete
- CS1574: resolve cref
```
2. **CacheInstructionService.cs** - TODO comments (multiple locations)
3. **MemberUserStore.cs** - TODO: Handle external logins
4. **BackOfficeUserStore.cs** - TODO: Optimize user queries
5. **UserRepository.cs** - TODO: SQLite session validation lock (line 39)
6. **Repository Base Classes** - Some repos have large inheritance chains (tech debt)
### TRACE_SCOPES Feature
**Debug-only scope tracing** (Umbraco.Infrastructure.csproj:34-36):
```xml
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
<DefineConstants>$(DefineConstants);TRACE_SCOPES</DefineConstants>
</PropertyGroup>
```
- Enables detailed scope logging in Debug builds
- Helps debug nested scope issues
- Performance impact - only use in development
---
## Quick Reference
### Essential Commands
```bash
# Build
dotnet build src/Umbraco.Infrastructure
# Test Infrastructure
dotnet test --filter "FullyQualifiedName~Infrastructure"
# Format code
dotnet format src/Umbraco.Infrastructure
# Pack for NuGet
dotnet pack src/Umbraco.Infrastructure -c Release
```
### Key Dependencies
- **Umbraco.Core** - Interface contracts (only project dependency)
- **NPoco** - Database access
- **Examine.Core** - Search indexing
- **MailKit** - Email sending
- **Serilog** - Logging
### Important Files
- **Project file**: `src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj`
- **Scope Provider**: `src/Umbraco.Infrastructure/Scoping/ScopeProvider.cs`
- **Database Factory**: `src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs`
- **Migration Executor**: `src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs`
- **Content Service**: `src/Umbraco.Infrastructure/Services/Implement/ContentService.cs`
- **User Repository**: `src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs`
### Critical Patterns
```csharp
// 1. Always use scopes for database operations
using (ICoreScope scope = ScopeProvider.CreateCoreScope())
{
// Work
scope.Complete(); // MUST call to commit
}
// 2. NPoco query building
Sql<ISqlContext> sql = Sql()
.Select<ContentDto>()
.From<ContentDto>()
.Where<ContentDto>(x => x.NodeId == id);
// 3. Repository pattern
IContent? content = _contentRepository.Get(id);
// 4. Service pattern with notifications
var saving = new ContentSavingNotification(content, eventMessages);
if (_eventAggregator.PublishCancelable(saving))
return Attempt.Fail(...); // Cancelled
_contentRepository.Save(content);
var saved = new ContentSavedNotification(content, eventMessages);
_eventAggregator.Publish(saved);
```
### Configuration
No appsettings in this library - all configuration from parent application (Umbraco.Web.UI):
- Connection strings
- Email settings
- Serilog configuration
- Background job schedules
### Getting Help
- **Core Docs**: `../Umbraco.Core/CLAUDE.md` (interface contracts)
- **Root Docs**: `/CLAUDE.md` (repository overview)
- **Official Docs**: https://docs.umbraco.com/umbraco-cms/reference/

View File

@@ -0,0 +1,89 @@
# Umbraco Backoffice - @umbraco-cms/backoffice
Modern TypeScript/Lit-based web components library for the Umbraco CMS backoffice interface. This project provides extensible UI components, APIs, and utilities for building the Umbraco CMS administration interface.
**Package**: `@umbraco-cms/backoffice`
**Version**: 17.1.0-rc
**License**: MIT
**Repository**: https://github.com/umbraco/Umbraco-CMS
**Live Preview**: https://backofficepreview.umbraco.com/
---
## Documentation Structure
This project's documentation is organized into 9 focused guides:
**Note**: This is a sub-project in the Umbraco CMS monorepo. For Git workflow, PR process, and CI/CD information, see the [repository root CLAUDE.md](../../CLAUDE.md).
### Architecture & Design
- **[Architecture](./docs/architecture.md)** - Technology stack, design patterns, module organization
### Development
- **[Commands](./docs/commands.md)** - Build, test, and development commands
### Code Quality
- **[Style Guide](./docs/style-guide.md)** - Naming and formatting conventions
- **[Clean Code](./docs/clean-code.md)** - Best practices and SOLID principles
- **[Testing](./docs/testing.md)** - Unit, integration, and E2E testing strategies
### Troubleshooting
- **[Error Handling](./docs/error-handling.md)** - Error patterns and debugging
- **[Edge Cases](./docs/edge-cases.md)** - Common pitfalls and gotchas
### Security & AI
- **[Security](./docs/security.md)** - XSS prevention, authentication, input validation
- **[Agentic Workflow](./docs/agentic-workflow.md)** - Three-phase AI development process
---
## Quick Start
### Prerequisites
- **Node.js**: >=22.17.1
- **npm**: >=10.9.2
- Modern browser (Chrome, Firefox, Safari)
### Initial Setup
```bash
# 1. Clone repository
git clone https://github.com/umbraco/Umbraco-CMS.git
cd Umbraco-CMS/src/Umbraco.Web.UI.Client
# 2. Install dependencies
npm install
# 3. Start development
npm run dev
```
### Most Common Commands
See **[Commands](./docs/commands.md)** for all available commands.
**Development**: `npm run dev` | **Testing**: `npm test` | **Build**: `npm run build` | **Lint**: `npm run lint:fix`
---
## Quick Reference
| Category | Details |
|----------|---------|
| **Apps** | `src/apps/` - Application entry points (app, installer, upgrader) |
| **Libraries** | `src/libs/` - Core APIs (element-api, context-api, controller-api) |
| **Packages** | `src/packages/` - Feature packages; `src/packages/core/` for utilities |
| **External** | `src/external/` - Dependency wrappers (lit, rxjs, luxon) |
| **Mocks** | `src/mocks/` - MSW handlers and mock data |
| **Config** | `package.json`, `vite.config.ts`, `.env` (create `.env.local`) |
| **Elements** | Custom elements use `umb-{feature}-{component}` pattern |
### Getting Help
**Documentation**: [UI API Docs](npm run generate:ui-api-docs) | [Storybook](npm run storybook) | [Official Docs](https://docs.umbraco.com/)
**Community**: [Issues](https://github.com/umbraco/Umbraco-CMS/issues) | [Discussions](https://github.com/umbraco/Umbraco-CMS/discussions) | [Forum](https://our.umbraco.com/)
---
**This project follows a modular package architecture with strict TypeScript, Lit web components, and an extensible manifest system. Each package is independent but follows consistent patterns. For extension development, use the Context API for dependency injection, controllers for logic, and manifests for registration.**

View File

@@ -0,0 +1,517 @@
# Agentic Workflow
[← Umbraco Backoffice](../CLAUDE.md) | [← Monorepo Root](../../CLAUDE.md)
---
### Phase 1: Analysis & Planning
**Understand Requirements**:
1. Read user request carefully
2. Identify acceptance criteria
3. Ask clarifying questions if ambiguous
4. Determine scope (new feature, bug fix, refactor, etc.)
**Research Codebase**:
1. Find similar implementations in codebase
2. Identify patterns and conventions used
3. Locate relevant packages and modules
4. Review existing tests for similar features
5. Check for existing utilities or helpers
**Identify Technical Approach**:
1. Which packages need changes?
- New package or modify existing?
- Core infrastructure or feature package?
2. What patterns to use?
- Web Component, Controller, Repository, Context?
- Extension type? (dashboard, workspace, modal, etc.)
3. Dependencies needed?
- New npm packages?
- Internal package dependencies?
4. API changes needed?
- New OpenAPI endpoints?
- Changes to existing endpoints?
**Break Down Implementation**:
1. **Models/Types** - Define TypeScript interfaces and types
2. **API Client** - Update OpenAPI client if backend changes
3. **Repository** - Data access layer
4. **Store/Context** - State management
5. **Controller** - Business logic
6. **Element** - UI component
7. **Manifest** - Extension registration
8. **Tests** - Unit and integration tests
9. **Documentation** - JSDoc and examples
10. **Storybook** (if applicable) - Component stories
**Consider Architecture**:
- Does this follow project patterns?
- Are dependencies correct? (libs → packages → apps)
- Will this create circular dependencies?
- Is this extensible for future needs?
- Performance implications?
**Document Plan**:
- Write brief implementation plan
- Identify potential issues or blockers
- Get approval from user if significant changes
### Phase 2: Incremental Implementation
**For New Feature (Component/Package)**:
**Step 1: Define Types**
```typescript
// 1. Create model interface
export interface UmbMyModel {
id: string;
name: string;
description?: string;
}
// 2. Create manifest type
export interface UmbMyManifest extends UmbManifestBase {
type: 'myType';
// ... specific properties
}
```
**Verify**: TypeScript compiles, no errors
**Step 2: Create Repository**
```typescript
// 3. Data access layer
export class UmbMyRepository {
async requestById(id: string) {
// Fetch from API
// Return { data } or { error }
}
}
```
**Verify**: Repository compiles, basic structure correct
**Step 3: Create Store/Context**
```typescript
// 4. State management
export class UmbMyStore extends UmbStoreBase {
// Observable state
}
export const UMB_MY_CONTEXT = new UmbContextToken<UmbMyContext>('UmbMyContext');
export class UmbMyContext extends UmbControllerBase {
#repository = new UmbMyRepository();
#store = new UmbMyStore(this);
// Public API
}
```
**Verify**: Context compiles, can be consumed
**Step 4: Create Element**
```typescript
// 5. UI component
@customElement('umb-my-element')
export class UmbMyElement extends UmbLitElement {
#context?: typeof UMB_MY_CONTEXT.TYPE;
constructor() {
super();
this.consumeContext(UMB_MY_CONTEXT, (context) => {
this.#context = context;
});
}
render() {
return html`<div>My Element</div>`;
}
}
```
**Verify**: Element renders in browser, no console errors
**Step 5: Wire Up Interactions**
```typescript
// 6. Connect user interactions to logic
@customElement('umb-my-element')
export class UmbMyElement extends UmbLitElement {
@state()
private _data?: UmbMyModel;
async #handleClick() {
const { data, error } = await this.#context?.load();
if (error) {
this._error = error;
return;
}
this._data = data;
}
render() {
return html`
<uui-button @click=${this.#handleClick} label="Load"></uui-button>
${this._data ? html`<p>${this._data.name}</p>` : nothing}
`;
}
}
```
**Verify**: Interactions work, data flows correctly
**Step 6: Add Extension Manifest**
```typescript
// 7. Register extension
export const manifest: UmbManifestMyType = {
type: 'myType',
alias: 'My.Extension',
name: 'My Extension',
element: () => import('./my-element.element.js'),
};
```
**Verify**: Extension loads, manifest is valid
**Step 7: Write Tests**
```typescript
// 8. Unit tests
describe('UmbMyElement', () => {
it('should render', async () => {
const element = await fixture(html`<umb-my-element></umb-my-element>`);
expect(element).to.exist;
});
it('should load data', async () => {
// Test data loading
});
});
```
**Verify**: Tests pass
**Step 8: Add Error Handling**
```typescript
// 9. Handle errors gracefully
async #handleClick() {
try {
this._loading = true;
const { data, error } = await this.#context?.load();
if (error) {
this._error = 'Failed to load data';
return;
}
this._data = data;
} catch (error) {
this._error = 'Unexpected error occurred';
console.error('Load failed:', error);
} finally {
this._loading = false;
}
}
```
**Step 9: Add localization**
```typescript
// 10. Add to src/Umbraco.Web.UI.Client/src/assets/lang/en.ts and other appropriate files
{
actions: {
load: 'Load'
}
}
// 11. Use the localize helper (`this.localize.term()`) in the element
render() {
return html`
<uui-button @click=${this.#handleClick} label=${this.localize.term('actions_load')></uui-button>
${this._data ? html`<p>${this._data.name}</p>` : ''}
`;
}
async #handleClick() {
try {
this._loading = true;
const { data, error } = await this.#context?.load();
if (error) {
this._error = this.localize.term('errors_receivedErrorFromServer');
return;
}
this._data = data;
} catch (error) {
this._error = this.localize.term('errors_defaultError');
console.error('Load failed:', error);
} finally {
this._loading = false;
}
}
// 12. Outside elements (such as controllers), use the Localization Controller
export class UmbMyController extends UmbControllerBase {
#localize = new UmbLocalizationController(this);
#notificationContext?: typeof UMB_NOTIFICATION_CONTEXT.TYPE;
constructor(host: UmbControllerHost) {
super(host);
this.consumeContext(UMB_NOTIFICATION_CONTEXT, (notificationContext) => {
this.#notificationContext = notificationContext;
});
}
fetchData() {
// Show error
this.#notificationContext?.peek('positive', {
data: {
headline: this.#localize.term('speechBubbles_onlineHeadline'),
message: this.#localize.term('speechBubbles_onlineMessage'),
},
});
}
}
**Verify**: Errors are handled, UI shows error state
**After Each Step**:
- Code compiles (no TypeScript errors)
- Tests pass (existing and new)
- Follows patterns (consistent with codebase)
- No breaking changes (or documented)
- ESLint passes
- Commit working code
### Phase 3: Review & Refinement
**Run All Quality Checks**:
```bash
# 1. Run all tests
npm test
# 2. Run lint
npm run lint:errors
# 3. Type check
npm run compile
# 4. Build
npm run build
# 5. Check circular dependencies
npm run check:circular
# 6. Validate package exports
npm run package:validate
```
**Code Review Checklist**:
- [ ] Code follows style guide
- [ ] All tests pass
- [ ] New tests added for new code
- [ ] TypeScript types are correct
- [ ] No `any` types (or justified)
- [ ] JSDoc comments on public APIs
- [ ] Error handling in place
- [ ] Edge cases handled
- [ ] No console.log in production code
- [ ] Accessibility considered (for UI components)
- [ ] Performance acceptable
- [ ] No memory leaks (subscriptions cleaned up)
- [ ] Follows existing patterns
- [ ] No circular dependencies introduced
**Documentation**:
- [ ] JSDoc on public APIs
- [ ] Update README if needed
- [ ] Add Storybook story (for components)
- [ ] Add example code (for APIs)
- [ ] Update CLAUDE.md (for significant changes)
### Reasoning & Decision-Making
**Architecture Decisions**:
- **Follow existing patterns** - Don't invent new patterns unless necessary
- **Use Context API** - For cross-component state and dependencies
- **Controllers for logic** - Separate business logic from UI
- **Repositories for data** - Abstract data access
- **Manifests for extensions** - Register features via extension system
**Component Design Decisions**:
- **Small components** - Break complex UI into smaller components
- **Single responsibility** - Each component does one thing well
- **Composition** - Combine small components to build complex UI
- **Props down, events up** - Data flows down via properties, changes flow up via events
**State Management Decisions**:
- **Local state** - `@state()` for component-specific state
- **Shared state** - Context and stores for cross-component state
- **Observables** - RxJS for reactive data streams
- **Immutability** - Don't mutate state, create new objects
**API Design Decisions**:
- **Async by default** - All data operations are async
- **Error objects** - Return `{ data, error }` not throw exceptions
- **TypeScript types** - Strong typing for all APIs
- **Consistent naming** - Follow existing naming conventions
### Think-Aloud Process
**Example: "I need to add a bulk action for content"**
**Analysis**:
> "I need to add a bulk action for content. Let me think through this:
>
> 1. What is a bulk action? It's an action that operates on multiple items.
> 2. Where do bulk actions appear? In collection views (document tree, list view).
> 3. What pattern is used? Let me check existing bulk actions...
> 4. Found: Block editor has bulk actions. Let me review that code.
> 5. Architecture: Bulk actions are registered via manifests (type: 'entityBulkAction')
> 6. Implementation needs:
> - Manifest to register action
> - Element to show confirmation dialog
> - Repository method to perform bulk operation
> - Permission check (can user do this?)
> 7. Which package? This goes in `packages/documents/documents/` since it's document-specific.
> 8. Dependencies: Need `@umbraco-cms/backoffice/entity-bulk-action`, modal, repository."
**Planning**:
> "Implementation steps:
> 1. Create bulk action manifest in documents package
> 2. Create modal element for confirmation
> 3. Add repository method for bulk operation
> 4. Add permission check
> 5. Wire up action to modal
> 6. Test with multiple documents
> 7. Handle errors (partial success, permissions, etc.)
> 8. Add loading state
> 9. Show success/error notification
> 10. Write tests"
**Implementation**:
> "Starting with step 1: Create manifest.
> Looking at existing bulk actions, I see the pattern uses `UmbEntityBulkActionBase`.
> Let me create the manifest following that pattern..."
### Error Recovery
**If Tests Fail**:
1. Read the error message carefully
2. Reproduce the failure locally
3. Debug with console.log or debugger
4. Understand root cause (don't just fix symptoms)
5. Fix the underlying issue
6. Verify fix doesn't break other tests
7. Add test for the bug (if it was a real bug)
**If Architecture Feels Wrong**:
1. Pause and reconsider
2. Review similar implementations in codebase
3. Discuss with team (via PR comments)
4. Don't force a pattern that doesn't fit
5. Refactor if needed
**If Introducing Breaking Changes**:
1. Discuss with user first
2. Document the breaking change
3. Provide migration path
4. Update CHANGELOG
5. Consider deprecation instead
**If Stuck**:
1. Ask for clarification from user
2. Review documentation
3. Look at similar code in repository
4. Break problem into smaller pieces
5. Try a simpler approach first
### Quality Gates Checklist
Before marking work as complete:
**Code Quality**:
- [ ] All new code has unit tests
- [ ] Integration tests for workflows (if applicable)
- [ ] All tests pass (including existing tests)
- [ ] Code follows style guide (ESLint passes)
- [ ] Prettier formatting correct
- [ ] No TypeScript errors
- [ ] JSDoc comments on public functions
- [ ] No `console.log` in production code
- [ ] No commented-out code
**Security**:
- [ ] Input validation in place
- [ ] No XSS vulnerabilities
- [ ] No sensitive data logged
- [ ] User input sanitized
- [ ] Permissions checked (for sensitive operations)
**Performance**:
- [ ] No obvious performance issues
- [ ] No memory leaks (subscriptions cleaned up)
- [ ] Efficient algorithms used
- [ ] Large lists use virtualization (if needed)
**Architecture**:
- [ ] Follows existing patterns
- [ ] No circular dependencies
- [ ] Correct package dependencies
- [ ] Extension manifest correct (if applicable)
**Documentation**:
- [ ] JSDoc on public APIs
- [ ] README updated (if needed)
- [ ] Examples added (if new API)
- [ ] Breaking changes documented
**User Experience** (for UI changes):
- [ ] Accessible (keyboard navigation, screen readers)
- [ ] Responsive (works on different screen sizes)
- [ ] Loading states shown
- [ ] Errors handled gracefully
- [ ] Success feedback provided
### Communication
**After Each Phase**:
- Summarize what was implemented
- Highlight key decisions and why
- Call out any blockers or questions
- Show progress (code snippets, screenshots for UI)
**When Making Decisions**:
- Explain reasoning
- Reference existing patterns
- Call out trade-offs
- Ask for confirmation on significant changes
**When Blocked**:
- Clearly state the blocker
- Explain what you've tried
- Ask specific questions
- Suggest potential approaches
**When Complete**:
- Summarize what was delivered
- Note any deviations from plan (and why)
- Highlight testing performed
- Mention any follow-up work needed
### For Large Features
**Multi-Session Features**:
1. Create feature branch (if spanning multiple PRs)
2. Break into multiple PRs if very large:
- PR 1: Infrastructure (types, repository, context)
- PR 2: UI components
- PR 3: Extensions and integration
3. Keep main stable - only merge completed, tested features
4. Consider feature flags for gradual rollout
5. Document progress and remaining work in PR description
**Coordination**:
- Update PR description with progress
- Use GitHub task lists in PR description
- Comment on PR with status updates
- Request early feedback on architecture
---

View File

@@ -0,0 +1,124 @@
# Architecture
[← Umbraco Backoffice](../CLAUDE.md) | [← Monorepo Root](../../CLAUDE.md)
---
### Technology Stack
- **Node.js**: >=22.17.1
- **npm**: >=10.9.2
- **Language**: TypeScript 5.9.3
- **Module System**: ESM (ES Modules)
- **Framework**: Lit (Web Components)
- **Build Tool**: Vite 7.1.11
- **Test Framework**: @web/test-runner with Playwright
- **E2E Testing**: Playwright 1.55.1
- **Code Quality**: ESLint 9.37.0, Prettier 3.6.2
- **Mocking**: MSW (Mock Service Worker) 1.3.5
- **Documentation**: Storybook 9.0.14, TypeDoc 0.28.13
### Application Type
Single-page web application (SPA) packaged as an npm library called `@umbraco-cms/backoffice` for typings and built as a bundle, which is copied over to `src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/backoffice` for the CMS. Provides:
- Extensible Web Components for CMS backoffice UI
- API libraries for extension development
- TypeScript type definitions
- Package manifest system for extensions
### Architecture Pattern
**Modular Package Architecture** with clear separation:
```
src/
├── apps/ # Application entry points
│ ├── app/ # Main backoffice application
│ ├── backoffice/ # Backoffice shell
│ ├── installer/ # CMS installer interface
│ ├── preview/ # Content preview
│ └── upgrader/ # CMS upgrader interface
├── libs/ # Core API libraries (infrastructure)
│ ├── class-api/ # Base class utilities
│ ├── context-api/ # Context API for dependency injection
│ ├── context-proxy/ # Context proxying utilities
│ ├── controller-api/ # Controller lifecycle management
│ ├── element-api/ # Element base classes & mixins
│ ├── extension-api/ # Extension registration & loading
│ ├── localization-api/ # Internationalization
│ └── observable-api/ # Reactive state management
├── packages/ # Feature packages (50+ packages)
│ ├── core/ # Core utilities (auth, http, router, etc.)
│ ├── content/ # Content management
│ ├── documents/ # Document types & editing
│ ├── media/ # Media management
│ ├── members/ # Member management
│ ├── user/ # User management
│ ├── templating/ # Templates, scripts, stylesheets
│ ├── block/ # Block editor components
│ └── ... # 30+ more specialized packages
├── external/ # External dependency wrappers
│ ├── lit/ # Lit framework wrapper
│ ├── rxjs/ # RxJS wrapper
│ ├── luxon/ # Date/time library wrapper
│ ├── monaco-editor/# Code editor wrapper
│ └── ... # Other wrapped dependencies
├── mocks/ # MSW mock handlers & test data
│ ├── data/ # Mock database
│ └── handlers/ # API request handlers
└── assets/ # Static assets (fonts, images, localization)
```
### Design Patterns
1. **Web Components** - Custom elements with Shadow DOM encapsulation
2. **Context API** - Dependency injection via context providers/consumers (similar to React Context)
3. **Controller Pattern** - Lifecycle-aware controllers for managing component behavior
4. **Extension System** - Manifest-based plugin architecture
5. **Observable Pattern** - Reactive state management with RxJS observables
6. **Repository Pattern** - Data access abstraction via repository classes
7. **Mixin Pattern** - Composable behaviors via TypeScript mixins (`UmbElementMixin`, etc.)
8. **Builder Pattern** - For complex object construction
9. **Registry Pattern** - Extension registry for dynamic feature loading
10. **Observer Pattern** - Event-driven communication between components
### Key Technologies
**Core Framework**:
- Lit 3.x - Web Components framework with reactive templates
- TypeScript 5.9 - Type-safe development with strict mode
- Vite - Fast build tool and dev server
**UI Components**:
- @umbraco-ui/uui - Umbraco UI component library
- Shadow DOM - Component style encapsulation
- Custom Elements API - Native web components
**State & Data**:
- RxJS - Reactive programming with observables
- Context API - State management & dependency injection
- MSW - API mocking for development & testing
**Testing**:
- @web/test-runner - Fast test runner for web components
- Playwright - E2E browser testing
- @open-wc/testing - Testing utilities for web components
**Code Quality**:
- ESLint with TypeScript plugin - Linting with strict rules
- Prettier - Code formatting
- TypeDoc - API documentation generation
- Web Component Analyzer - Custom element documentation
**Other**:
- Luxon - Date/time manipulation
- Monaco Editor - Code editing
- DOMPurify - HTML sanitization
- Marked - Markdown parsing
- SignalR - Real-time communication

View File

@@ -0,0 +1,397 @@
# Clean Code
[← Umbraco Backoffice](../CLAUDE.md) | [← Monorepo Root](../../CLAUDE.md)
---
### Function Design
**Function Length**:
- Target: ≤30 lines per function
- Max: 50 lines (enforce via code review)
- Extract complex logic to separate functions
- Use early returns to reduce nesting
**Single Responsibility**:
```typescript
// Good - Single responsibility
private _validateName(name: string): boolean {
return name.length > 0 && name.length <= 100;
}
private _sanitizeName(name: string): string {
return name.trim().replace(/[<>]/g, '');
}
// Bad - Multiple responsibilities
private _processName(name: string): { valid: boolean; sanitized: string } {
// Doing too much in one function
}
```
**Descriptive Names**:
- Use verb names for functions: `loadData`, `validateInput`, `handleClick`
- Boolean functions: `is`, `has`, `can`, `should` prefix
- Event handlers: `handle`, `on` prefix: `handleSubmit`, `onClick`
**Parameters**:
- Limit to 3-4 parameters
- Use options object for more parameters:
```typescript
// Good - Options object
interface LoadOptions {
id: string;
includeChildren?: boolean;
depth?: number;
culture?: string;
}
private _load(options: LoadOptions) { }
// Bad - Too many parameters
private _load(id: string, includeChildren: boolean, depth: number, culture: string) { }
```
**Early Returns**:
```typescript
// Good - Early returns reduce nesting
private _validate(): boolean {
if (!this._data) return false;
if (!this._data.name) return false;
if (this._data.name.length === 0) return false;
return true;
}
// Bad - Nested conditions
private _validate(): boolean {
if (this._data) {
if (this._data.name) {
if (this._data.name.length > 0) {
return true;
}
}
}
return false;
}
```
### Class Design
**Single Responsibility**:
- Each class should have one reason to change
- Controllers handle one aspect of behavior
- Repositories handle one entity type
- Components handle one UI concern
**Small Classes**:
- Target: <300 lines per class
- Extract complex logic to separate controllers/utilities
- Use composition over inheritance
**Encapsulation**:
```typescript
export class UmbMyElement extends LitElement {
// Public API - reactive properties
@property({ type: String })
value = '';
// Private state
@state()
private _loading = false;
// Private fields (not reactive)
#controller = new UmbMyController(this);
// Private methods
private _loadData() { }
}
```
### SOLID Principles (Adapted for TypeScript/Lit)
**S - Single Responsibility**:
- One component = one UI responsibility
- One controller = one behavior responsibility
- One repository = one entity type
**O - Open/Closed**:
- Extend via composition and mixins
- Extension API for plugins
- Avoid modifying existing components, create new ones
**L - Liskov Substitution**:
- Subclasses should honor base class contracts
- Use interfaces for polymorphism
**I - Interface Segregation**:
- Small, focused interfaces
- Use TypeScript `interface` for contracts
**D - Dependency Inversion**:
- Depend on abstractions (interfaces) not concrete classes
- Use Context API for dependency injection
- Controllers receive dependencies via constructor
### Dependency Injection
**Context API** (Preferred):
```typescript
export class UmbMyElement extends UmbElementMixin(LitElement) {
#authContext?: UmbAuthContext;
constructor() {
super();
// Consume context (dependency injection)
this.consumeContext(UMB_AUTH_CONTEXT, (context) => {
this.#authContext = context;
});
}
}
```
**Controller Pattern**:
```typescript
export class UmbMyController extends UmbControllerBase {
#repository: UmbContentRepository;
constructor(host: UmbControllerHost, repository: UmbContentRepository) {
super(host);
this.#repository = repository;
}
}
// Usage
#controller = new UmbMyController(this, new UmbContentRepository());
```
### Avoid Code Smells
**Magic Numbers/Strings**:
```typescript
// Bad
if (status === 200) { }
if (type === 'document') { }
// Good
const HTTP_OK = 200;
const CONTENT_TYPE_DOCUMENT = 'document';
if (status === HTTP_OK) { }
if (type === CONTENT_TYPE_DOCUMENT) { }
// Or use enums
enum ContentType {
Document = 'document',
Media = 'media',
}
```
**Long Parameter Lists**:
```typescript
// Bad
function create(name: string, type: string, parent: string, culture: string, template: string) { }
// Good
interface CreateOptions {
name: string;
type: string;
parent?: string;
culture?: string;
template?: string;
}
function create(options: CreateOptions) { }
```
**Duplicate Code**:
- Extract to shared functions
- Use composition and mixins
- Create utility modules
**Deeply Nested Code**:
- Use early returns
- Extract to separate functions
- Use guard clauses
**Callback Hell** (N/A - use async/await)
### Modern Patterns
**Async/Await**:
```typescript
// Good - Clean async code
async loadContent() {
try {
this._loading = true;
const { data, error } = await this.repository.requestById(this.id);
if (error) {
this._error = error;
return;
}
this._content = data;
} finally {
this._loading = false;
}
}
```
**Optional Chaining**:
```typescript
// Good - Safe property access
const name = this._content?.variants?.[0]?.name;
// Bad - Manual null checks
const name = this._content && this._content.variants &&
this._content.variants[0] && this._content.variants[0].name;
```
**Destructuring**:
```typescript
// Good - Destructure for clarity
const { name, description, icon } = this._content;
// Good - With defaults
const { name = 'Untitled', description = '' } = this._content;
```
**Immutability**:
```typescript
// Good - Spread operator for immutability
this._items = [...this._items, newItem];
this._config = { ...this._config, newProp: value };
// Bad - Mutation
this._items.push(newItem);
this._config.newProp = value;
```
**Pure Functions**:
```typescript
// Good - Pure function (no side effects)
function calculateTotal(items: Item[]): number {
return items.reduce((sum, item) => sum + item.price, 0);
}
// Bad - Impure (modifies input)
function calculateTotal(items: Item[]): number {
items.sort((a, b) => a.price - b.price); // Mutation!
return items.reduce((sum, item) => sum + item.price, 0);
}
```
### TypeScript-Specific Patterns
**Type Guards**:
```typescript
function isContentModel(value: unknown): value is UmbContentModel {
return typeof value === 'object' && value !== null && 'id' in value;
}
// Usage
if (isContentModel(data)) {
// TypeScript knows data is UmbContentModel
console.log(data.id);
}
```
**Discriminated Unions**:
```typescript
type Result<T> =
| { success: true; data: T }
| { success: false; error: Error };
function handleResult<T>(result: Result<T>) {
if (result.success) {
console.log(result.data); // TypeScript knows this exists
} else {
console.error(result.error); // TypeScript knows this exists
}
}
```
**Utility Types**:
```typescript
// Partial - Make all properties optional
type PartialContent = Partial<UmbContentModel>;
// Pick - Select specific properties
type ContentSummary = Pick<UmbContentModel, 'id' | 'name' | 'icon'>;
// Omit - Remove specific properties
type ContentWithoutId = Omit<UmbContentModel, 'id'>;
// Readonly - Make immutable
type ReadonlyContent = Readonly<UmbContentModel>;
```
### Comments and Documentation
**When to Comment**:
- Explain "why" not "what"
- Document complex algorithms
- JSDoc for public APIs
- Warn about gotchas or non-obvious behavior
**JSDoc for Web Components**:
```typescript
/**
* A button component that triggers document actions.
* @element umb-document-action-button
* @fires {CustomEvent} action-click - Fired when action is clicked
* @slot - Default slot for button content
* @cssprop --umb-button-color - Button text color
*/
export class UmbDocumentActionButton extends LitElement {
/**
* The action identifier
* @attr
* @type {string}
*/
@property({ type: String })
action = '';
}
```
**TODOs**:
```typescript
// TODO: Implement pagination [NL]
// FIXME: Memory leak in subscription [JOV]
// HACK: Temporary workaround for API bug [LK]
```
**Remove Dead Code**:
- Don't comment out code, delete it (Git history preserves it)
- Remove unused imports, functions, variables
- Clean up console.logs before committing
### Patterns to Avoid
**Don't**:
- Use `var` (use `const`/`let`)
- Modify prototypes of built-in objects
- Use global variables
- Block the main thread (use web workers for heavy computation)
- Create deeply nested structures
- Use `any` type (use `unknown` or proper types)
- Use non-null assertions `!` unless absolutely necessary
- Ignore TypeScript errors with `@ts-ignore`

View File

@@ -0,0 +1,214 @@
# Commands
[← Umbraco Backoffice](../CLAUDE.md) | [← Monorepo Root](../../CLAUDE.md)
---
### Installation
```bash
# Install dependencies (must use npm, not yarn/pnpm due to workspaces)
npm install
# Note: Requires Node >=22.17.1 and npm >=10.9.2
```
### Build Commands
```bash
# TypeScript compilation only
npm run build
# Build for CMS (production build + copy to .NET project)
npm run build:for:cms
# Build for npm distribution (with type declarations)
npm run build:for:npm
# Build with Vite (alternative build method)
npm run build:vite
# Build workspaces
npm run build:workspaces
# Build Storybook documentation
npm run build-storybook
```
### Development Commands
```bash
# Start dev server (with live reload)
npm run dev
# Start dev server connected to real backend
npm run dev:server
# Start dev server with MSW mocks (default)
npm run dev:mock
# Preview production build
npm run preview
```
### Test Commands
```bash
# Run all unit tests
npm test
# Run tests in watch mode
npm run test:watch
# Run tests in development mode
npm run test:dev
# Run tests in watch mode (dev config)
npm run test:dev-watch
# Run E2E tests with Playwright
npm run test:e2e
# Run example tests
npm run test:examples
# Run example tests in watch mode
npm run test:examples:watch
# Run example tests in browser
npm run test:examples:browser
```
### Code Quality Commands
```bash
# Lint TypeScript files
npm run lint
# Lint and show only errors
npm run lint:errors
# Lint and auto-fix issues
npm run lint:fix
# Format code
npm run format
# Format and auto-fix
npm run format:fix
# Type check
npm run compile
# Run all checks (lint, compile, build-storybook, jsonschema)
npm run check
```
### Code Generation Commands
```bash
# Generate TypeScript config
npm run generate:tsconfig
# Generate OpenAPI client from backend API
npm run generate:server-api
# Generate icons
npm run generate:icons
# Generate package manifest
npm run generate:manifest
# Generate JSON schema for umbraco-package.json
npm run generate:jsonschema
# Generate JSON schema to dist
npm run generate:jsonschema:dist
# Generate UI API docs with TypeDoc
npm run generate:ui-api-docs
# Generate const check tests
npm run generate:check-const-test
```
### Analysis Commands
```bash
# Check for circular dependencies
npm run check:circular
# Check module dependencies
npm run check:module-dependencies
# Check path lengths
npm run check:paths
# Analyze web components
npm run wc-analyze
# Analyze web components for VS Code
npm run wc-analyze:vscode
```
### Storybook Commands
```bash
# Start Storybook dev server
npm run storybook
# Build Storybook
npm run storybook:build
# Build and preview Storybook
npm run storybook:preview
```
### Package Management
```bash
# Validate package exports
npm run package:validate
# Prepare for npm publish
npm run prepack
```
### Environment Setup
**Prerequisites**:
- Node.js >=22.17.1
- npm >=10.9.2
- Modern browser (Chrome, Firefox, Safari)
**Initial Setup**:
1. Clone repository
```bash
git clone https://github.com/umbraco/Umbraco-CMS.git
cd Umbraco-CMS/src/Umbraco.Web.UI.Client
```
2. Install dependencies
```bash
npm install
```
3. Configure environment (optional)
```bash
cp .env .env.local
# Edit .env.local with your settings
```
4. Start development
```bash
npm run dev
```
**Environment Variables** (see `.env` file):
- `VITE_UMBRACO_USE_MSW` - Enable/disable Mock Service Worker (`on`/`off`)
- `VITE_UMBRACO_API_URL` - Backend API URL (e.g., `https://localhost:44339`)
- `VITE_UMBRACO_INSTALL_STATUS` - Install status (`running`, `must-install`, `must-upgrade`)
- `VITE_UMBRACO_EXTENSION_MOCKS` - Enable extension mocks (`on`/`off`)

View File

@@ -0,0 +1,584 @@
# Edge Cases
[← Umbraco Backoffice](../CLAUDE.md) | [← Monorepo Root](../../CLAUDE.md)
---
### Null/Undefined Handling
**Always Check**:
```typescript
// Use optional chaining
const name = this._content?.name;
// Use nullish coalescing for defaults
const name = this._content?.name ?? 'Untitled';
// Check before accessing
if (!this._content) {
return html`<p>No content</p>`;
}
```
**TypeScript Strict Null Checks** (enabled):
- Variables are non-nullable by default
- Use `Type | undefined` or `Type | null` explicitly
- TypeScript forces null checks
**Function Parameters**:
```typescript
// Make nullable parameters explicit
function load(id: string, culture?: string) {
const cultureCode = culture ?? 'en-US'; // Default value
}
```
### Array Edge Cases
**Empty Arrays**:
```typescript
// Check length before access
if (this._items.length === 0) {
return html`<p>No items</p>`;
}
// Safe array methods
const first = this._items[0]; // Could be undefined
const first = this._items.at(0); // Also could be undefined
// Use optional chaining
const firstId = this._items[0]?.id;
```
**Single vs Multiple Items**:
```typescript
// Handle both cases
const items = Array.isArray(data) ? data : [data];
```
**Array Methods on Undefined**:
```typescript
// Guard against undefined
const ids = this._items?.map(item => item.id) ?? [];
// Or check first
if (!this._items) {
return [];
}
return this._items.map(item => item.id);
```
**Sparse Arrays** (rare in this codebase):
```typescript
// Use filter to remove empty slots
const dense = sparse.filter(() => true);
```
### String Edge Cases
**Empty Strings**:
```typescript
// Check for empty strings
if (!name || name.trim().length === 0) {
return 'Untitled';
}
// Or use default
const displayName = name?.trim() || 'Untitled';
```
**String vs Null/Undefined**:
```typescript
// Distinguish between empty string and null
const hasValue = value !== null && value !== undefined;
const isEmpty = value === '';
// Or use optional chaining
const length = value?.length ?? 0;
```
**Trim Whitespace**:
```typescript
// Always trim user input
const cleanName = this._name.trim();
// Validate after trimming
if (cleanName.length === 0) {
// Invalid
}
```
**String Encoding**:
- Use UTF-8 everywhere
- Be aware of Unicode characters (emojis, etc.)
- Use `textContent` not `innerHTML` for plain text
**Internationalization**:
```typescript
// Use localization API
const label = this.localize.term('general_submit');
// Not hardcoded strings
// const label = 'Submit'; // ❌
```
### Number Edge Cases
**NaN Checks**:
```typescript
// Use Number.isNaN, not isNaN
if (Number.isNaN(value)) {
// Handle NaN
}
// isNaN coerces, Number.isNaN doesn't
isNaN('hello'); // true (coerces to NaN)
Number.isNaN('hello'); // false (not a number)
```
**Infinity**:
```typescript
if (!Number.isFinite(value)) {
// Handle Infinity or NaN
}
```
**Parsing**:
```typescript
// parseInt/parseFloat can return NaN
const num = parseInt(input, 10);
if (Number.isNaN(num)) {
// Handle invalid input
}
// Or use Number constructor with validation
const num = Number(input);
if (!Number.isFinite(num)) {
// Invalid
}
```
**Floating Point Precision**:
```typescript
// Don't compare floats with ===
const isEqual = Math.abs(a - b) < 0.0001;
// Or use integers for currency (cents, not dollars)
const priceInCents = 1099; // $10.99
```
**Division by Zero**:
```typescript
// JavaScript returns Infinity, not error
const result = 10 / 0; // Infinity
// Check denominator
if (denominator === 0) {
// Handle division by zero
return 0; // Or throw error
}
```
**Safe Integer Range**:
```typescript
// JavaScript integers are safe up to Number.MAX_SAFE_INTEGER
const isSafe = Number.isSafeInteger(value);
// For IDs, use strings not numbers
interface UmbContentModel {
id: string; // Not number
}
```
### Object Edge Cases
**Property Existence**:
```typescript
// Use optional chaining
const value = obj?.property;
// Or check explicitly
if ('property' in obj) {
const value = obj.property;
}
// hasOwnProperty (not inherited)
if (Object.hasOwn(obj, 'property')) {
// Property exists on object itself
}
```
**Null vs Undefined vs {}**:
```typescript
// Distinguish between missing and empty
const isEmpty = obj !== null && obj !== undefined && Object.keys(obj).length === 0;
// Or use optional chaining
const hasData = obj && Object.keys(obj).length > 0;
```
**Shallow vs Deep Copy**:
```typescript
// Shallow copy
const copy = { ...original };
// Deep copy (for simple objects)
const deepCopy = JSON.parse(JSON.stringify(original));
// Deep copy with structuredClone (modern browsers)
const deepCopy = structuredClone(original);
// Note: Functions, symbols, and undefined are not copied by JSON.stringify
```
**Object Freezing**:
```typescript
// Prevent modification
const frozen = Object.freeze(obj);
// Check if frozen
if (Object.isFrozen(obj)) {
// Can't modify
}
```
### Async/Await Edge Cases
**Unhandled Promise Rejections**:
```typescript
// Always catch errors
try {
await asyncOperation();
} catch (error) {
console.error('Failed:', error);
}
// Or use .catch()
asyncOperation().catch(error => {
console.error('Failed:', error);
});
// For fire-and-forget, explicitly catch
void asyncOperation().catch(error => console.error(error));
```
**Promise.all Fails Fast**:
```typescript
// Promise.all rejects if ANY promise rejects
try {
const results = await Promise.all([op1(), op2(), op3()]);
} catch (error) {
// One failed, others may still be running
}
// Use Promise.allSettled to wait for all (even if some fail)
const results = await Promise.allSettled([op1(), op2(), op3()]);
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`Op ${index} succeeded:`, result.value);
} else {
console.error(`Op ${index} failed:`, result.reason);
}
});
```
**Race Conditions**:
```typescript
// Avoid race conditions with sequential operations
this._loading = true;
try {
const data1 = await fetchData1();
const data2 = await fetchData2(data1.id);
this._result = processData(data1, data2);
} finally {
this._loading = false;
}
// For parallel operations, use Promise.all
const [data1, data2] = await Promise.all([fetchData1(), fetchData2()]);
```
**Timeout Handling**:
```typescript
// Implement timeout for operations
function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
return Promise.race([
promise,
new Promise<T>((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), ms)
),
]);
}
// Usage
try {
const data = await withTimeout(fetchData(), 5000);
} catch (error) {
// Handle timeout or other errors
}
```
**Memory Leaks with Event Listeners**:
```typescript
export class UmbMyElement extends LitElement {
#controller = new UmbMyController(this);
// Controllers automatically clean up on disconnect
constructor() {
super();
this.#controller.observe(dataSource$, (value) => {
this._data = value;
});
}
// Lit lifecycle handles this automatically
disconnectedCallback() {
super.disconnectedCallback();
// Controllers are destroyed automatically
}
}
```
### Web Component Edge Cases
**Custom Element Not Defined**:
```typescript
// Check if element is defined
if (!customElements.get('umb-my-element')) {
// Not defined yet
await customElements.whenDefined('umb-my-element');
}
// Or use upgrade
await customElements.upgrade(element);
```
**Shadow DOM Queries**:
```typescript
// Query shadow root, not document
const button = this.shadowRoot?.querySelector('button');
// Use Lit decorators
@query('#myButton')
private _button!: HTMLButtonElement;
```
**Property vs Attribute Sync**:
```typescript
// Lit keeps properties and attributes in sync
@property({ type: String })
name = ''; // Syncs with name="" attribute
// But complex types don't sync to attributes
@property({ type: Object })
data = {}; // No attribute sync
// State doesn't sync to attributes
@state()
private _loading = false;
```
**Reactive Update Timing**:
```typescript
// Wait for update to complete
this.value = 'new value';
await this.updateComplete;
// Now DOM is updated
// Or use requestUpdate
this.requestUpdate();
await this.updateComplete;
```
### Date/Time Edge Cases
**Use Luxon** (not Date):
```typescript
import { DateTime } from '@umbraco-cms/backoffice/external/luxon';
// Create dates
const now = DateTime.now();
const utc = DateTime.utc();
const parsed = DateTime.fromISO('2024-01-15T10:30:00Z');
// Always store dates in UTC
const isoString = now.toUTC().toISO();
// Format for display
const formatted = now.toLocaleString(DateTime.DATETIME_MED);
// Timezone handling
const local = utc.setZone('local');
```
**Date Comparison**:
```typescript
// Compare DateTime objects
if (date1 < date2) { }
if (date1.equals(date2)) { }
// Or compare timestamps
if (date1.toMillis() < date2.toMillis()) { }
```
**Date Parsing Can Fail**:
```typescript
const date = DateTime.fromISO(input);
if (!date.isValid) {
console.error('Invalid date:', date.invalidReason);
// Handle invalid date
}
```
### JSON Edge Cases
**JSON.parse Can Throw**:
```typescript
// Always wrap in try/catch
try {
const data = JSON.parse(jsonString);
} catch (error) {
console.error('Invalid JSON:', error);
// Handle parse error
}
```
**Circular References**:
```typescript
// JSON.stringify throws on circular references
const obj = { a: 1 };
obj.self = obj;
try {
JSON.stringify(obj);
} catch (error) {
// TypeError: Converting circular structure to JSON
}
```
**Date Objects**:
```typescript
// Dates become strings
const data = { date: new Date() };
const json = JSON.stringify(data);
const parsed = JSON.parse(json);
// parsed.date is a string, not Date
// Use ISO format explicitly
const isoDate = new Date().toISOString();
```
**Undefined Values**:
```typescript
// Undefined values are omitted
const obj = { a: 1, b: undefined };
JSON.stringify(obj); // '{"a":1}'
// Use null for explicit absence
const obj = { a: 1, b: null };
JSON.stringify(obj); // '{"a":1,"b":null}'
```
### Handling Strategy
**Guard Clauses**:
```typescript
// Check preconditions early
if (!this._data) return;
if (this._data.length === 0) return;
if (!this._data[0].name) return;
// Now can safely use data
this.processData(this._data[0].name);
```
**Defensive Programming**:
```typescript
// Validate inputs
function process(items: unknown) {
if (!Array.isArray(items)) {
throw new Error('Expected array');
}
// Safe to use items as array
}
// Use type guards
if (isContentModel(data)) {
// TypeScript knows data is UmbContentModel
}
```
**Fail Fast**:
```typescript
// Throw errors early for programmer mistakes
if (this._repository === undefined) {
throw new Error('Repository not initialized');
}
// Handle expected errors gracefully
try {
const data = await this._repository.loadById(id);
} catch (error) {
this._error = 'Failed to load content';
return;
}
```
**Document Edge Cases**:
```typescript
/**
* Loads content by ID.
* @throws {UmbContentNotFoundError} If content doesn't exist
* @throws {UmbUnauthorizedError} If user lacks permission
* @returns {Promise<UmbContentModel>} The content model
*
* @remarks
* This method returns cached data if available.
* Pass `{ skipCache: true }` to force a fresh load.
*/
async loadById(id: string, options?: LoadOptions): Promise<UmbContentModel> {
// ...
}
```

View File

@@ -0,0 +1,242 @@
# Error Handling
[← Umbraco Backoffice](../CLAUDE.md) | [← Monorepo Root](../../CLAUDE.md)
---
### Diagnosis Process
1. **Check browser console** for errors and warnings
2. **Reproduce consistently** - Identify exact steps
3. **Check network tab** for failed API calls
4. **Inspect element** to verify DOM structure
5. **Check Lit reactive update cycle** - Use `element.updateComplete`
6. **Verify context availability** - Check context providers
7. **Review event listeners** - Check event propagation
8. **Use browser debugger** with source maps
**Common Web Component Issues**:
- Component not rendering: Check if custom element is defined
- Properties not updating: Verify `@property()` decorator and reactive update cycle
- Events not firing: Check event names and listeners
- Shadow DOM issues: Use `shadowRoot.querySelector()` not `querySelector()`
- Styles not applied: Check Shadow DOM style encapsulation
- Context not available: Ensure context provider is ancestor in DOM tree
### Error Handling Standards
**Error Classes**:
```typescript
// Use built-in Error class or extend it
throw new Error('Failed to load content');
// Custom error classes for domain errors
export class UmbContentNotFoundError extends Error {
constructor(id: string) {
super(`Content with id "${id}" not found`);
this.name = 'UmbContentNotFoundError';
}
}
```
**Repository Pattern Error Handling**:
```typescript
async requestById(id: string): Promise<{ data?: UmbContentModel; error?: Error }> {
try {
const response = await this._apiClient.getById({ id });
return { data: response.data };
} catch (error) {
return { error: error as Error };
}
}
// Usage
const { data, error } = await repository.requestById('123');
if (error) {
// Handle error
console.error('Failed to load content:', error);
return;
}
// Use data
```
**Observable Error Handling**:
```typescript
this.observe(dataSource$, (value) => {
// Success handler
this._data = value;
}).catch((error) => {
// Error handler
console.error('Observable error:', error);
});
```
**Promise Error Handling**:
```typescript
// Always use try/catch with async/await
async myMethod() {
try {
const result = await this.fetchData();
return result;
} catch (error) {
console.error('Failed to fetch data:', error);
throw error; // Re-throw or handle
}
}
// Or use .catch()
this.fetchData()
.then(result => this.handleResult(result))
.catch(error => this.handleError(error));
```
### Web Component Error Handling
**Lifecycle Errors**:
```typescript
export class UmbMyElement extends UmbElementMixin(LitElement) {
constructor() {
try {
super();
// Initialization that might throw
} catch (error) {
console.error('Failed to initialize element:', error);
}
}
async connectedCallback() {
try {
super.connectedCallback();
// Async initialization
await this.loadData();
} catch (error) {
console.error('Failed to connect element:', error);
this._errorMessage = 'Failed to load component';
}
}
}
```
**Render Errors**:
```typescript
override render() {
if (this._error) {
return html`<umb-error-message .error=${this._error}></umb-error-message>`;
}
if (!this._data) {
return html`<uui-loader></uui-loader>`;
}
return html`
<!-- Normal render -->
`;
}
```
### Logging Standards
**Console Logging**:
```typescript
// Development only - Remove before production
console.log('Debug info:', data);
// Errors - Kept in production but sanitized
console.error('Failed to load:', error);
// Warnings
console.warn('Deprecated API usage:', method);
// Avoid console.log in production code (ESLint warning)
```
**Custom Logging** (if needed):
```typescript
// Use debug flag for verbose logging
if (this._debug) {
console.log('[UmbMyComponent]', 'State changed:', this._state);
}
```
**Don't Log Sensitive Data**:
- User credentials
- API tokens
- Personal information (PII)
- Session IDs
- Full error stack traces in production
### Development Environment
- **Source Maps**: Enabled in Vite config for debugging
- **Error Overlay**: Vite provides error overlay in development
- **Hot Module Replacement**: Instant feedback on code changes
- **Detailed Errors**: Full stack traces with source locations
- **TypeScript Checking**: Real-time type checking in IDE
### Production Environment
- **Sanitized Errors**: No stack traces exposed to users
- **User-Friendly Messages**: Show helpful error messages
- **Error Boundaries**: Catch errors at component boundaries
- **Graceful Degradation**: Fallback UI when errors occur
- **Error Reporting**: Log errors to console (browser dev tools)
**Production Error Display**:
```typescript
private _errorMessage?: string;
override render() {
if (this._errorMessage) {
return html`
<uui-box>
<p class="error">${this._errorMessage}</p>
<uui-button @click=${this._retry} label="Try Again"></uui-button>
</uui-box>
`;
}
// ... normal render
}
```
### Context-Specific Error Handling
**HTTP Client Errors** (OpenAPI):
```typescript
try {
const response = await this._apiClient.getDocument({ id });
return response.data;
} catch (error) {
if (error.status === 404) {
throw new UmbContentNotFoundError(id);
}
if (error.status === 403) {
throw new UmbUnauthorizedError('Access denied');
}
throw error;
}
```
**Observable Subscription Errors**:
```typescript
this._subscription = this._dataSource.asObservable().subscribe({
next: (value) => this._data = value,
error: (error) => {
console.error('Observable error:', error);
this._errorMessage = 'Failed to load data';
},
complete: () => console.log('Observable completed'),
});
```

View File

@@ -0,0 +1,299 @@
# Security
[← Umbraco Backoffice](../CLAUDE.md) | [← Monorepo Root](../../CLAUDE.md)
---
### Input Validation
**Validate All User Input**:
```typescript
// Use validation in forms
import { UmbValidationController } from '@umbraco-cms/backoffice/validation';
#validation = new UmbValidationController(this);
async #handleSubmit() {
if (!this.#validation.validate()) {
return; // Show validation errors
}
// Proceed with submission
}
```
**String Validation**:
```typescript
private _validateName(name: string): boolean {
// Length check
if (name.length === 0 || name.length > 100) {
return false;
}
// Pattern check (example: alphanumeric and spaces)
if (!/^[a-zA-Z0-9\s]+$/.test(name)) {
return false;
}
return true;
}
```
**Sanitize HTML**:
```typescript
// Use DOMPurify for HTML sanitization
import DOMPurify from '@umbraco-cms/backoffice/external/dompurify';
const cleanHtml = DOMPurify.sanitize(userInput);
// In Lit templates, use unsafeHTML directive with sanitized content
import { unsafeHTML } from '@umbraco-cms/backoffice/external/lit';
render() {
return html`<div>${unsafeHTML(DOMPurify.sanitize(this.htmlContent))}</div>`;
}
```
### Authentication & Authorization
**OpenID Connect** via backend:
- Backoffice uses OpenID Connect for authentication
- Authentication handled by .NET backend
- Tokens managed by browser (httpOnly cookies)
**Authorization Checks**:
```typescript
// Check user permissions before actions
#authContext?: UmbAuthContext;
async #handleDelete() {
const hasPermission = await this.#authContext?.hasPermission('delete');
if (!hasPermission) {
// Show error or hide action
return;
}
// Proceed with deletion
}
```
**Context Security**:
- Use Context API for auth state
- Don't store sensitive tokens in localStorage
- Backend handles token refresh
### API Security
**HTTP Client Security**:
```typescript
// OpenAPI client handles:
// - CSRF tokens
// - Request headers
// - Credentials
// - Error handling
// Use generated OpenAPI client
import { ContentResource } from '@umbraco-cms/backoffice/external/backend-api';
const client = new ContentResource();
const response = await client.getById({ id });
```
**CORS** (Backend Configuration):
- Configured in .NET backend
- Backoffice follows same-origin policy
- API calls to same origin
**Rate Limiting** (Backend):
- Handled by .NET backend
- Backoffice respects rate limit headers
### XSS Prevention
**Template Security** (Lit):
```typescript
// Lit automatically escapes content in templates
render() {
// Safe - Automatically escaped
return html`<div>${this.userContent}</div>`;
// UNSAFE - Only use with sanitized content
return html`<div>${unsafeHTML(DOMPurify.sanitize(this.htmlContent))}</div>`;
}
```
**Attribute Binding**:
```typescript
// Safe - Lit escapes attribute values
render() {
return html`<input value=${this.userInput} />`;
}
```
**Event Handlers**:
```typescript
// Safe - Event handlers are not strings
render() {
return html`<button @click=${this.#handleClick}>Click</button>`;
}
// NEVER do this (code injection risk)
// render() {
// return html`<button onclick="${this.userCode}">Click</button>`;
// }
```
### Content Security Policy
**CSP Headers** (Backend Configuration):
- Configured in .NET backend
- Restricts script sources
- Prevents inline scripts (except with nonce)
- Reports violations
**Backoffice Compliance**:
- No inline scripts
- No `eval()` or `Function()` constructor
- Monaco Editor uses web workers (CSP compliant)
### Dependencies Security
**Package Management**:
```bash
# Check for vulnerabilities
npm audit
# Fix automatically
npm audit fix
# Update dependencies carefully
npm update
```
**Dependency Security Practices**:
- Renovate bot automatically creates PRs for updates
- Review dependency changes before merging
- Only use packages from npm registry
- Verify package integrity
- Keep dependencies updated
**Known Vulnerabilities**:
- CI checks for vulnerabilities on every PR
- Security advisories reviewed regularly
### Common Vulnerabilities
**XSS (Cross-Site Scripting)**:
- ✅ Lit templates automatically escape content
- ✅ DOMPurify for HTML sanitization
- ❌ Never use `unsafeHTML` with user input directly
- ❌ Never set `innerHTML` with user input
**CSRF (Cross-Site Request Forgery)**:
- ✅ Backend sends CSRF tokens
- ✅ OpenAPI client includes tokens automatically
- ✅ SameSite cookies
**Injection Attacks**:
- ✅ Backend uses parameterized queries
- ✅ Input validation on both frontend and backend
- ✅ OpenAPI client prevents injection
**Prototype Pollution**:
- ❌ Never use `Object.assign` with user input as source
- ❌ Never use `_.merge` with untrusted data
- ✅ Validate object shapes before using
**ReDoS (Regular Expression Denial of Service)**:
- ✅ Review complex regex patterns
- ✅ Test regex with long inputs
- ❌ Avoid backtracking in regex
### Secure Coding Practices
**Don't Trust Client Data**:
- Validate on backend (primary defense)
- Frontend validation is UX, not security
**Principle of Least Privilege**:
- Only request permissions needed
- Check permissions before sensitive operations
- Hide UI for unavailable actions
**Sanitize Output**:
- Always sanitize HTML before rendering
- Escape special characters in user content
- Use Lit's automatic escaping
**Secure Defaults**:
- Forms should validate by default
- Sensitive operations require confirmation
- Errors don't expose sensitive information
**Defense in Depth**:
- Multiple layers of security
- Frontend validation + Backend validation
- Input sanitization + Output escaping
- Authentication + Authorization
### Security Anti-Patterns to Avoid
**Never do this**:
```typescript
// XSS vulnerability
element.innerHTML = userInput;
// Code injection
eval(userCode);
// Exposing sensitive data
console.log('Token:', authToken);
// Storing secrets
localStorage.setItem('apiKey', key);
// Disabling validation
// @ts-ignore
// eslint-disable-next-line
// Trusting user input
const url = userInput; // Could be javascript:alert()
window.location.href = url;
```
**Do this instead**:
```typescript
// Safe HTML rendering
element.textContent = userInput;
// or
render() {
return html`<div>${userInput}</div>`;
}
// No eval needed
// Use proper JavaScript patterns
// Don't log sensitive data
console.log('Operation completed');
// Backend manages secrets
// Frontend receives tokens via httpOnly cookies
// Fix TypeScript/ESLint issues properly
// Don't suppress warnings
// Validate and sanitize URLs
const url = new URL(userInput, window.location.origin);
if (url.protocol === 'https:' || url.protocol === 'http:') {
window.location.href = url.href;
}
```

View File

@@ -0,0 +1,157 @@
# Style Guide
[← Umbraco Backoffice](../CLAUDE.md) | [← Monorepo Root](../../CLAUDE.md)
---
### Naming Conventions
**Files**:
- Web components: `my-component.element.ts`
- Tests: `my-component.test.ts`
- Stories: `my-component.stories.ts`
- Controllers: `my-component.controller.ts`
- Contexts: `my-component.context.ts`
- Modals: `my-component.modal.ts`
- Workspaces: `my-component.workspace.ts`
- Repositories: `my-component.repository.ts`
- Index files: `index.ts` (barrel exports)
**Classes & Types**:
- Classes: `PascalCase` with `Umb` prefix: `UmbMyComponent`
- Interfaces: `PascalCase` with `Umb` prefix: `UmbMyInterface`
- Types: `PascalCase` with `Umb`, `Ufm`, `Manifest`, `Meta`, or `Example` prefix
- Exported types MUST have approved prefix
- Example types for docs: `ExampleMyType`
**Variables & Functions**:
- Public members: `camelCase` without underscore: `myVariable`, `myMethod`
- Private members: `camelCase` with leading underscore: `_myPrivateVariable`
- #private members: `camelCase` without underscore: `#myPrivateField`
- Protected members: `camelCase` with optional underscore: `myProtected` or `_myProtected`
- Constants (exported): `UPPER_SNAKE_CASE` with `UMB_` prefix: `UMB_MY_CONSTANT`
- Local constants: `UPPER_CASE` or `camelCase`
**Custom Elements**:
- Element tag names: kebab-case with `umb-` prefix: `umb-my-component`
- Must be registered in global `HTMLElementTagNameMap`
### File Organization
- One class/component per file
- Use barrel exports (`index.ts`) for package public APIs
- Import order (enforced by ESLint):
1. External dependencies
2. Parent imports
3. Sibling imports
4. Index imports
5. Type-only imports (separate)
### Code Formatting (Prettier)
```json
{
"printWidth": 120,
"singleQuote": true,
"semi": true,
"bracketSpacing": true,
"bracketSameLine": true,
"useTabs": true
}
```
- **Indentation**: Tabs (not spaces)
- **Line length**: 120 characters max
- **Quotes**: Single quotes
- **Semicolons**: Required
- **Trailing commas**: Yes (default)
### TypeScript Conventions
**Strict Mode** (enabled in `tsconfig.json`):
- `strict: true`
- `noImplicitReturns: true`
- `noFallthroughCasesInSwitch: true`
- `noImplicitOverride: true`
**Type Features**:
- Use TypeScript types over JSDoc when possible
- BUT: Lit components use JSDoc for web-component-analyzer compatibility
- Use `type` for unions/intersections: `type MyType = A | B`
- Use `interface` for object shapes and extension: `interface MyInterface extends Base`
- Prefer `const` over `let`, never use `var`
- Use `readonly` for immutable properties
- Use generics for reusable code
- Avoid `any` (lint warning), use `unknown` instead
- Use type guards for narrowing
- Use `as const` for literal types
**Module Syntax**:
- ES Modules only: `import`/`export`
- Use consistent type imports: `import type { MyType } from '...'`
- Use consistent type exports: `export type { MyType }`
- No side-effects in imports
**Decorators**:
- `@customElement('umb-my-element')` - Register custom element
- `@property({ type: String })` - Reactive properties
- `@state()` - Internal reactive state
- `@query('#myId')` - Query shadow DOM
- Experimental decorators enabled
### Modern TypeScript Features to Use
- **Async/await** over callbacks
- **Optional chaining**: `obj?.property?.method?.()`
- **Nullish coalescing**: `value ?? defaultValue`
- **Template literals**: `` `Hello ${name}` ``
- **Destructuring**: `const { a, b } = obj`
- **Spread operator**: `{ ...obj, newProp: value }`
- **Arrow functions**: `const fn = () => {}`
- **Array methods**: `map`, `filter`, `reduce`, `find`, `some`, `every`
- **Object methods**: `Object.keys`, `Object.values`, `Object.entries`
- **Private fields**: `#privateField`
### Language Features to Avoid
- `var` (use `const`/`let`)
- `eval()` or `Function()` constructor
- `with` statement
- `arguments` object (use rest parameters)
- Deeply nested callbacks (use async/await)
- Non-null assertions unless absolutely necessary: `value!`
- Type assertions unless necessary: `value as Type`
- `@ts-ignore` (use `@ts-expect-error` with comment)
### Custom ESLint Rules
**Project-Specific Rules**:
- `prefer-static-styles-last` - Static styles property must be last in class
- `enforce-umbraco-external-imports` - External dependencies must be imported via `@umbraco-cms/backoffice/external/*`
- Private members MUST have leading underscore
- Exported types MUST have approved prefix (Umb, Ufm, Manifest, Meta, Example)
- Exported string constants MUST have UMB_ prefix
- Semicolons required
- No `var` keyword
- No circular dependencies (max depth 6)
- No self-imports
- Consistent type imports/exports
**Allowed JSDoc Tags** (for web-component-analyzer):
- `@element` - Element name
- `@attr` - HTML attribute
- `@fires` - Custom events
- `@prop` - Properties
- `@slot` - Slots
- `@cssprop` - CSS custom properties
- `@csspart` - CSS parts
### Documentation
- **Public APIs**: JSDoc comments with `@description`, `@param`, `@returns`, `@example`
- **Web Components**: JSDoc with web-component-analyzer tags
- **Complex logic**: Inline comments explaining "why" not "what"
- **TODOs**: Format as `// TODO: description [initials]`
- **Deprecated**: Use `@deprecated` tag with migration instructions

View File

@@ -0,0 +1,279 @@
# Testing
[← Umbraco Backoffice](../CLAUDE.md) | [← Monorepo Root](../../CLAUDE.md)
---
### Testing Philosophy
- **Unit tests** for business logic and utilities
- **Component tests** for web components
- **Integration tests** for workflows
- **E2E tests** for critical user journeys
- **Coverage target**: No strict requirement, focus on meaningful tests
- **Test pyramid**: Many unit tests, fewer integration tests, few E2E tests
### Testing Frameworks
**Unit/Component Testing**:
- `@web/test-runner` - Fast test runner for web components
- `@open-wc/testing` - Testing utilities, includes Chai assertions
- `@types/chai` - Assertion library
- `@types/mocha` - Test framework (used by @web/test-runner)
- `@web/test-runner-playwright` - Browser launcher
- `element-internals-polyfill` - Polyfill for form-associated custom elements
**E2E Testing**:
- `@playwright/test` - End-to-end testing in real browsers
- Playwright MSW integration for API mocking
**Test Utilities**:
- `@umbraco-cms/internal/test-utils` - Shared test utilities
- MSW (Mock Service Worker) - API mocking
- Fixtures in `src/mocks/data/` - Test data
### Test Project Organization
```
src/
├── **/*.test.ts # Unit tests co-located with source
├── mocks/
│ ├── data/ # Mock data & in-memory databases
│ └── handlers/ # MSW request handlers
├── examples/
│ └── **/*.test.ts # Example tests
└── utils/
└── test-utils.ts # Shared test utilities
e2e/
├── **/*.spec.ts # Playwright E2E tests
└── fixtures/ # E2E test fixtures
```
### Test Naming Convention
```typescript
describe('UmbMyComponent', () => {
describe('initialization', () => {
it('should create element', async () => {
// test
});
it('should set default properties', async () => {
// test
});
});
describe('user interactions', () => {
it('should emit event when button clicked', async () => {
// test
});
});
});
```
### Test Structure (AAA Pattern)
```typescript
it('should do something', async () => {
// Arrange - Set up test data and conditions
const element = await fixture<UmbMyElement>(html`<umb-my-element></umb-my-element>`);
const spy = sinon.spy();
element.addEventListener('change', spy);
// Act - Perform the action
element.value = 'new value';
await element.updateComplete;
// Assert - Verify the results
expect(spy.calledOnce).to.be.true;
expect(element.value).to.equal('new value');
});
```
### Unit Test Guidelines
**What to Test**:
- Public API methods and properties
- User interactions (clicks, inputs, etc.)
- State changes
- Event emissions
- Error handling
- Edge cases
**What NOT to Test**:
- Private implementation details
- Framework/library code
- Generated code (e.g., OpenAPI clients)
- Third-party dependencies
**Best Practices**:
- One assertion per test (when practical)
- Test behavior, not implementation
- Use meaningful test names (describe what should happen)
- Keep tests fast (<100ms each)
- Isolate tests (no shared state between tests)
- Use fixtures for DOM elements
- Use spies/stubs for external dependencies
- Clean up after tests (auto-handled by @web/test-runner)
**Web Component Testing**:
```typescript
import { fixture, html, expect } from '@open-wc/testing';
import { UmbMyElement } from './my-element.element';
describe('UmbMyElement', () => {
let element: UmbMyElement;
beforeEach(async () => {
element = await fixture(html`<umb-my-element></umb-my-element>`);
});
it('should render', () => {
expect(element).to.exist;
expect(element.shadowRoot).to.exist;
});
it('should be accessible', async () => {
await expect(element).to.be.accessible();
});
});
```
### Integration Test Guidelines
Integration tests verify interactions between multiple components/systems:
```typescript
describe('UmbContentRepository', () => {
let repository: UmbContentRepository;
let mockContext: UmbMockContext;
beforeEach(() => {
mockContext = new UmbMockContext();
repository = new UmbContentRepository(mockContext);
});
it('should fetch and cache content', async () => {
const result = await repository.requestById('content-123');
expect(result.data).to.exist;
// Verify caching behavior
const cached = await repository.requestById('content-123');
expect(cached.data).to.equal(result.data);
});
});
```
### E2E Test Guidelines
E2E tests with Playwright:
```typescript
import { test, expect } from '@playwright/test';
test.describe('Content Editor', () => {
test('should create new document', async ({ page }) => {
await page.goto('/');
await page.click('[data-test="create-document"]');
await page.fill('[data-test="document-name"]', 'My New Page');
await page.click('[data-test="save"]');
await expect(page.locator('[data-test="success-notification"]')).toBeVisible();
});
});
```
### Mocking Best Practices
**MSW (Mock Service Worker)** for API mocking:
```typescript
import { rest } from 'msw';
export const handlers = [
rest.get('/umbraco/management/api/v1/document/:id', (req, res, ctx) => {
const { id } = req.params;
return res(
ctx.json({
id,
name: 'Test Document',
// ... mock data
})
);
}),
];
```
**Context Mocking**:
```typescript
import { UmbMockContext } from '@umbraco-cms/internal/test-utils';
const mockContext = new UmbMockContext();
mockContext.provideContext(UMB_AUTH_CONTEXT, mockAuthContext);
```
### Running Tests
**Local Development**:
```bash
# Run all tests once
npm test
# Run in watch mode
npm run test:watch
# Run with dev config (faster, less strict)
npm run test:dev
# Run in watch mode with dev config
npm run test:dev-watch
# Run specific test file pattern
npm test -- --files "**/my-component.test.ts"
```
**E2E Tests**:
```bash
# Run E2E tests
npm run test:e2e
# Run in headed mode (see browser)
npx playwright test --headed
# Run specific test
npx playwright test e2e/content-editor.spec.ts
# Debug mode
npx playwright test --debug
```
**CI/CD**:
- All tests run on pull requests
- E2E tests run on Chromium in CI
- Retries: 2 attempts on CI, 0 locally
- Parallel: Sequential in CI, parallel locally
### Coverage
Coverage reporting is currently disabled (see `web-test-runner.config.mjs`):
```javascript
/* TODO: fix coverage report
coverageConfig: {
reporters: ['lcovonly', 'text-summary'],
},
*/
```
**What to Exclude from Coverage**:
- Test files themselves
- Mock data and handlers
- Generated code (OpenAPI clients, icons)
- External wrapper modules
- Type declaration files