Add Claude memory files for all relevant project files (#20959)

* Regenerate delivery api claud memory file for updated file lines and inclusion of Secure Cookie-Based Token Storage

* Add delivery api memory file

* claude memory file for in memory modelsbuilder project

* Claud memory file for Imagesharp project

* Claude memory file for legacy image sharp project

* Claude memory files for Persistence projects

* Remaining claude memory files
This commit is contained in:
Sven Geusens
2025-11-27 10:47:19 +01:00
committed by GitHub
parent eaf5960a4d
commit c61bcca066
17 changed files with 4166 additions and 78 deletions

View File

@@ -23,7 +23,7 @@ Shared infrastructure for Umbraco CMS REST APIs (Management and Delivery).
- `Umbraco.Core` - Domain models and service contracts
- `Umbraco.Web.Common` - Web functionality
### Project Structure (45 files)
### Project Structure (46 files)
```
Umbraco.Cms.Api.Common/
@@ -55,20 +55,7 @@ Umbraco.Cms.Api.Common/
## 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
```
See "Quick Reference" section at bottom for common commands.
---
@@ -89,20 +76,22 @@ public class SchemaIdHandler : ISchemaIdHandler
**Why**: Management and Delivery APIs can customize schema/operation ID generation.
### Schema ID Sanitization (OpenApi/SchemaIdHandler.cs:32)
### Schema ID Sanitization (OpenApi/SchemaIdHandler.cs:24-29, 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)
// Add "Model" suffix to avoid TypeScript name clashes (lines 24-29)
if (name.EndsWith("Model") == false)
{
// because some models names clash with common classes in TypeScript (i.e. Document),
// we need to add a "Model" postfix to all models
name = $"{name}Model";
}
// Remove invalid characters to prevent OpenAPI generation errors (line 32)
return Regex.Replace(name, @"[^\w]", string.Empty);
```
### Polymorphic Deserialization (Serialization/UmbracoJsonTypeInfoResolver.cs:31-34)
### Polymorphic Deserialization (Serialization/UmbracoJsonTypeInfoResolver.cs:29-35)
```csharp
// IMPORTANT: do NOT return an empty enumerable here. it will cause nullability to fail on reference
@@ -144,8 +133,10 @@ dotnet test tests/Umbraco.Tests.Integration/
### Key Configuration (DependencyInjection/UmbracoBuilderAuthExtensions.cs)
**Reference Tokens over JWT** (line 73-74):
**Reference Tokens over JWT** (line 76-80):
```csharp
// Enable reference tokens
// - see https://documentation.openiddict.com/configuration/token-storage.html
options
.UseReferenceAccessTokens()
.UseReferenceRefreshTokens();
@@ -153,23 +144,44 @@ options
**Why**: More secure (revocable), better for load balancing, uses ASP.NET Core Data Protection.
**Token Lifetime** (line 84-85):
**Token Lifetime** (line 88-91):
```csharp
// Access token: 25% of refresh token lifetime
// Make the access token lifetime 25% of the refresh token lifetime
options.SetAccessTokenLifetime(new TimeSpan(timeOut.Ticks / 4));
options.SetRefreshTokenLifetime(timeOut);
```
**PKCE Required** (line 54-56):
**PKCE Required** (line 59-63):
```csharp
// Enable authorization code flow with PKCE
options
.AllowAuthorizationCodeFlow()
.RequireProofKeyForCodeExchange();
.RequireProofKeyForCodeExchange()
.AllowRefreshTokenFlow();
```
**Endpoints**:
- Backoffice: `/umbraco/management/api/v1/security/*`
- Member: `/umbraco/member/api/v1/security/*`
**Endpoints**: Backoffice `/umbraco/management/api/v1/security/*`, Member `/umbraco/member/api/v1/security/*`
### Secure Cookie-Based Token Storage (v17+)
**Implementation** (DependencyInjection/HideBackOfficeTokensHandler.cs):
Back-office tokens are hidden from client-side JavaScript via HTTP-only cookies:
```csharp
private const string AccessTokenCookieKey = "__Host-umbAccessToken";
private const string RefreshTokenCookieKey = "__Host-umbRefreshToken";
// Tokens are encrypted via Data Protection and stored in cookies
SetCookie(httpContext, AccessTokenCookieKey, context.Response.AccessToken);
context.Response.AccessToken = "[redacted]"; // Client sees redacted value
```
**Key Security Features** (lines 143-165): `HttpOnly`, `IsEssential`, `Path="/"`, `Secure` (HTTPS), `__Host-` prefix
**Configuration**: `BackOfficeTokenCookieSettings.Enabled` (default: true in v17+)
**Implications**: Client-side cannot access tokens; encrypted with Data Protection; load balancing needs shared key ring; API requests need `credentials: include`
---
@@ -183,10 +195,9 @@ options
```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));
// This happens when trying to deserialize to an interface, without sending the $type as part of the request
context.ModelState.TryAddModelException(string.Empty, new InputFormatterException(exception.Message, exception));
return await InputFormatterResult.FailureAsync();
}
```
@@ -196,23 +207,23 @@ catch (NotSupportedException exception)
**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";
}
```
**Solution**: Add "Model" suffix (OpenApi/SchemaIdHandler.cs:24-29)
### Generic Type Handling
**Issue**: `PagedViewModel<T>` needs flattened schema name.
**Solution** (OpenApi/SchemaIdHandler.cs:41-49):
**Solution** (OpenApi/SchemaIdHandler.cs:41-50):
```csharp
// Turns "PagedViewModel<RelationItemViewModel>" into "PagedRelationItemModel"
return $"{name}{string.Join(string.Empty, type.GenericTypeArguments.Select(SanitizedTypeName))}";
private string HandleGenerics(string name, Type type)
{
if (!type.IsGenericType)
return name;
// use attribute custom name or append the generic type names
// turns "PagedViewModel<RelationItemViewModel>" into "PagedRelationItem"
return $"{name}{string.Join(string.Empty, type.GenericTypeArguments.Select(SanitizedTypeName))}";
}
```
---
@@ -258,16 +269,6 @@ 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.
@@ -278,12 +279,7 @@ return BadRequest(problemDetails);
### 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.
**Why**: Cache discovered subtypes (UmbracoJsonTypeInfoResolver.cs:14) to avoid expensive reflection calls
### Known Limitations
@@ -318,23 +314,11 @@ private readonly ConcurrentDictionary<Type, ISet<Type>> _subTypesCache = new();
### Configuration
**HTTPS** (Configuration/ConfigureOpenIddict.cs:14):
```csharp
// Disable transport security requirement for local development
options.DisableTransportSecurityRequirement = _globalSettings.Value.UseHttps is false;
```
**HTTPS**: `DisableTransportSecurityRequirement` for local dev only (ConfigureOpenIddict.cs:14). **Warning**: Never disable in production.
**⚠️ Warning**: Never disable HTTPS in production.
### Usage Pattern
### 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
```
Consuming APIs call `builder.AddUmbracoApiOpenApiUI().AddUmbracoOpenIddict()`
---
@@ -343,7 +327,7 @@ builder
### Essential Commands
```bash
# Build
# Build project
dotnet build src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj
# Pack for NuGet
@@ -351,6 +335,10 @@ dotnet pack src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj -c Release
# Test via integration tests
dotnet test tests/Umbraco.Tests.Integration/
# Check packages
dotnet list src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj package --outdated
dotnet list src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj package --vulnerable
```
### Key Classes
@@ -361,13 +349,14 @@ dotnet test tests/Umbraco.Tests.Integration/
| `SchemaIdHandler` | Generate OpenAPI schema IDs | OpenApi/SchemaIdHandler.cs |
| `UmbracoJsonTypeInfoResolver` | Polymorphic JSON serialization | Serialization/UmbracoJsonTypeInfoResolver.cs |
| `UmbracoBuilderAuthExtensions` | Configure OpenIddict | DependencyInjection/UmbracoBuilderAuthExtensions.cs |
| `HideBackOfficeTokensHandler` | Secure cookie-based token storage | DependencyInjection/HideBackOfficeTokensHandler.cs |
| `PagedViewModel<T>` | Generic pagination model | ViewModels/Pagination/PagedViewModel.cs |
### Important Files
- `Umbraco.Cms.Api.Common.csproj` - Project dependencies
- `DependencyInjection/UmbracoBuilderApiExtensions.cs` - OpenAPI registration (line 12-30)
- `DependencyInjection/UmbracoBuilderAuthExtensions.cs` - OpenIddict setup (line 19-144)
- `DependencyInjection/UmbracoBuilderApiExtensions.cs` - OpenAPI registration (line 12-31)
- `DependencyInjection/UmbracoBuilderAuthExtensions.cs` - OpenIddict setup (line 20-183)
- `Security/Paths.cs` - API endpoint path constants
### Getting Help

View File

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

View File

@@ -0,0 +1,258 @@
# Umbraco.Cms.DevelopmentMode.Backoffice
Development-time library enabling **InMemoryAuto** ModelsBuilder mode with runtime Razor view compilation. Allows content type changes to instantly regenerate strongly-typed models without application restart.
---
## 1. Architecture
**Type**: Class Library (NuGet Package)
**Target Framework**: .NET 10.0
**Purpose**: Enable hot-reload of ModelsBuilder models during development
### Key Technologies
- **Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation** - Runtime Razor view compilation
- **Microsoft.CodeAnalysis.CSharp** (Roslyn) - Runtime C# compilation
- **AssemblyLoadContext** - Collectible assembly loading/unloading
### Dependencies
- `Umbraco.Web.Common` - Umbraco web infrastructure
### Project Structure (18 source files)
```
Umbraco.Cms.DevelopmentMode.Backoffice/
├── DependencyInjection/
│ ├── BackofficeDevelopmentComposer.cs # Auto-registration via IComposer
│ └── UmbracoBuilderExtensions.cs # DI setup (CRITICAL: contains 80-line design overview)
└── InMemoryAuto/
├── InMemoryModelFactory.cs # Core factory - generates/loads models (876 lines)
├── InMemoryAssemblyLoadContextManager.cs # Manages collectible AssemblyLoadContext
├── RoslynCompiler.cs # Compiles generated C# to DLL
├── CollectibleRuntimeViewCompiler.cs # Custom IViewCompiler for Razor (489 lines)
├── UmbracoViewCompilerProvider.cs # Provides CollectibleRuntimeViewCompiler
├── RuntimeCompilationCacheBuster.cs # Clears Razor caches on model rebuild
├── UmbracoRazorReferenceManager.cs # Manages Roslyn MetadataReferences
├── CompilationOptionsProvider.cs # Mirrors host app compilation settings
├── UmbracoAssemblyLoadContext.cs # Collectible AssemblyLoadContext wrapper
├── ChecksumValidator.cs # Validates precompiled view checksums
├── CompilationExceptionFactory.cs # Creates detailed compilation errors
├── UmbracoCompilationException.cs # Custom exception with CompilationFailures
├── ModelsBuilderAssemblyAttribute.cs # Marks InMemory assemblies
├── ModelsBuilderBindingErrorHandler.cs # Handles model binding version mismatches
├── InMemoryModelsBuilderModeValidator.cs # Validates runtime mode configuration
└── ModelsModeConstants.cs # "InMemoryAuto" constant
```
### Design Patterns
1. **Clone-and-Own Pattern** - ASP.NET Core's RuntimeViewCompiler and related classes are cloned because internal APIs can't be extended
2. **Collectible AssemblyLoadContext** - Enables assembly unloading for hot-reload
3. **FileSystemWatcher** - Monitors `~/umbraco/Data/TEMP/InMemoryAuto/` for changes
4. **Lazy Initialization** - References resolved on first use, not startup
---
## 2. Key Patterns
### Why Clone-and-Own (CRITICAL CONTEXT)
The 80-line comment in `DependencyInjection/UmbracoBuilderExtensions.cs:13-80` explains why this project exists. Key points:
1. **Problem**: ASP.NET Core's `RuntimeViewCompiler` loads assemblies into the default `AssemblyLoadContext`, which can't reference collectible contexts (breaking change in .NET 7)
2. **Failed Solutions**:
- Reflection to clear caches (unstable, internal APIs)
- Service wrapping via DI (still loads into wrong context)
3. **Solution**: Clone `RuntimeViewCompiler`, `RazorReferenceManager`, `ChecksumValidator`, and related classes, modifying them to:
- Load compiled views into the same collectible `AssemblyLoadContext` as models
- Explicitly add InMemoryAuto models assembly reference during compilation
### InMemoryModelFactory Lifecycle
```
Content Type Changed → Reset() called → _pendingRebuild = true
Next View Request → EnsureModels() → GetModelsAssembly(forceRebuild: true)
GenerateModelsCode() → RoslynCompiler.CompileToFile()
ReloadAssembly() → InMemoryAssemblyLoadContextManager.RenewAssemblyLoadContext()
RuntimeCompilationCacheBuster.BustCache() → Views recompile with new models
```
### Assembly Caching Strategy
Models are cached to disk for faster boot (`InMemoryModelFactory.cs:400-585`):
1. **Hash Check**: `TypeModelHasher.Hash(typeModels)` creates hash of content type definitions
2. **Cache Files** (in `~/umbraco/Data/TEMP/InMemoryAuto/`):
- `models.hash` - Current content type hash
- `models.generated.cs` - Generated source
- `all.generated.cs` - Combined source with assembly attributes
- `all.dll.path` - Path to compiled DLL
- `Compiled/generated.cs{hash}.dll` - Compiled assembly
3. **Boot Sequence**:
- Check if `models.hash` matches current hash
- If match, load existing DLL
- If mismatch or missing, recompile
### Collectible AssemblyLoadContext
```csharp
// InMemoryAssemblyLoadContextManager.cs:29-37
internal void RenewAssemblyLoadContext()
{
_currentAssemblyLoadContext?.Unload(); // Unload previous
_currentAssemblyLoadContext = new UmbracoAssemblyLoadContext(); // Create new collectible
}
```
**Key constraint**: No external references to the `AssemblyLoadContext` allowed - prevents unloading.
---
## 3. Error Handling
### Compilation Failures
`CompilationExceptionFactory.cs` creates `UmbracoCompilationException` with:
- Source file content
- Generated code
- Diagnostic messages with line/column positions
Errors logged in `CollectibleRuntimeViewCompiler.cs:383-396` and `InMemoryModelFactory.cs:341-354`.
### Model Binding Errors
`ModelsBuilderBindingErrorHandler.cs` handles version mismatches between:
- View's model type (from compiled view assembly)
- Content's model type (from InMemoryAuto assembly)
Reports detailed error messages including assembly versions.
---
## 4. Security
**Runtime Mode Validation**: `InMemoryModelsBuilderModeValidator.cs` prevents `InMemoryAuto` mode outside `BackofficeDevelopment` runtime mode.
**Temp File Location**: Models compiled to `~/umbraco/Data/TEMP/InMemoryAuto/` - ensure this directory isn't web-accessible.
---
## 5. Edge Cases & Known Issues
### Technical Debt (TODOs)
1. **Circular Reference** - `InMemoryModelFactory.cs:46`:
```csharp
private readonly Lazy<UmbracoServices> _umbracoServices; // TODO: this is because of circular refs :(
```
2. **DynamicMethod Performance** - `InMemoryModelFactory.cs:698-701`:
```csharp
// TODO: use Core's ReflectionUtilities.EmitCtor !!
// Yes .. DynamicMethod is uber slow
```
### Race Conditions
`InMemoryModelFactory.cs:800-809` - FileSystemWatcher can cause race conditions on slow cloud filesystems. Own file changes are always ignored.
### Unused Assembly Cleanup
`InMemoryModelFactory.cs:587-612` - `TryDeleteUnusedAssemblies` may fail with `UnauthorizedAccessException` if files are locked. Cleanup retried on next rebuild.
### Reflection for Cache Clearing
`RuntimeCompilationCacheBuster.cs:50-51` uses reflection to call internal `RazorViewEngine.ClearCache()`:
```csharp
Action<RazorViewEngine>? clearCacheMethod = ReflectionUtilities.EmitMethod<Action<RazorViewEngine>>("ClearCache");
```
---
## 6. Configuration
**Enable InMemoryAuto mode** (appsettings.json):
```json
{
"Umbraco": {
"CMS": {
"Runtime": {
"Mode": "BackofficeDevelopment"
},
"ModelsBuilder": {
"ModelsMode": "InMemoryAuto"
}
}
}
}
```
**Requirements**:
- `RuntimeMode` must be `BackofficeDevelopment`
- `ModelsMode` must be `InMemoryAuto`
---
## Quick Reference
### Essential Commands
```bash
# Build
dotnet build src/Umbraco.Cms.DevelopmentMode.Backoffice/Umbraco.Cms.DevelopmentMode.Backoffice.csproj
# Pack for NuGet
dotnet pack src/Umbraco.Cms.DevelopmentMode.Backoffice/Umbraco.Cms.DevelopmentMode.Backoffice.csproj -c Release
# Run integration tests
dotnet test tests/Umbraco.Tests.Integration/ --filter "FullyQualifiedName~ModelsBuilder"
```
**Note**: This library has no direct tests - tested via integration tests in `Umbraco.Tests.Integration`.
**Focus areas when modifying**:
- Assembly loading/unloading cycles
- Razor view compilation with models
- Cache invalidation timing
- Model binding error scenarios
### Essential Classes
| Class | Purpose | File |
|-------|---------|------|
| `InMemoryModelFactory` | Core model generation/loading | `InMemoryAuto/InMemoryModelFactory.cs` |
| `CollectibleRuntimeViewCompiler` | Custom Razor compiler | `InMemoryAuto/CollectibleRuntimeViewCompiler.cs` |
| `InMemoryAssemblyLoadContextManager` | Collectible assembly management | `InMemoryAuto/InMemoryAssemblyLoadContextManager.cs` |
| `RuntimeCompilationCacheBuster` | Cache invalidation | `InMemoryAuto/RuntimeCompilationCacheBuster.cs` |
| `UmbracoBuilderExtensions` | DI setup + design rationale | `DependencyInjection/UmbracoBuilderExtensions.cs` |
### Important Files
- **Design Overview**: `DependencyInjection/UmbracoBuilderExtensions.cs:13-80` - READ THIS FIRST
- **Project File**: `Umbraco.Cms.DevelopmentMode.Backoffice.csproj`
- **Model Factory**: `InMemoryAuto/InMemoryModelFactory.cs` - Most complex class
### Cloned Microsoft Code
These files are clones of ASP.NET Core internals - check upstream for updates:
- `ChecksumValidator.cs` - https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Razor.RuntimeCompilation/src/ChecksumValidator.cs
- `UmbracoRazorReferenceManager.cs` - https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Razor.RuntimeCompilation/src/RazorReferenceManager.cs
- `CompilationOptionsProvider.cs` - Partial clone of CSharpCompiler
- `CompilationExceptionFactory.cs` - Partial clone of CompilationFailedExceptionFactory
### Getting Help
- **Root Documentation**: `/CLAUDE.md`
- **Core Patterns**: `/src/Umbraco.Core/CLAUDE.md`
- **Official Docs**: https://docs.umbraco.com/umbraco-cms/reference/configuration/modelsbuildersettings
---
**This library enables hot-reload of content models during development. The core complexity is working around ASP.NET Core's internal Razor compilation APIs. Always read the design overview in `UmbracoBuilderExtensions.cs:13-80` before making changes.**

View File

@@ -0,0 +1,234 @@
# Umbraco.Cms.Imaging.ImageSharp
Image processing library using **ImageSharp 3.x** and **ImageSharp.Web** for on-the-fly image manipulation, resizing, cropping, and caching.
---
## 1. Architecture
**Type**: Class Library (NuGet Package)
**Target Framework**: .NET 10.0
**Purpose**: Provide image manipulation via query string parameters
### Key Technologies
- **SixLabors.ImageSharp** - Image processing library
- **SixLabors.ImageSharp.Web** - ASP.NET Core middleware for query string-based image manipulation
### Dependencies
- `Umbraco.Web.Common` - Web infrastructure
### Project Structure (7 source files)
```
Umbraco.Cms.Imaging.ImageSharp/
├── ImageSharpComposer.cs # Auto-registration via IComposer
├── UmbracoBuilderExtensions.cs # DI setup and middleware configuration
├── ConfigureImageSharpMiddlewareOptions.cs # Middleware options (caching, HMAC, size limits)
├── ConfigurePhysicalFileSystemCacheOptions.cs # File cache location
├── ImageProcessors/
│ └── CropWebProcessor.cs # Custom crop processor with EXIF awareness
└── Media/
├── ImageSharpDimensionExtractor.cs # Extract image dimensions (EXIF-aware)
└── ImageSharpImageUrlGenerator.cs # Generate query string URLs for processing
```
### Relationship to ImageSharp2
Two imaging packages exist:
- **Umbraco.Cms.Imaging.ImageSharp** (this package) - Uses ImageSharp 3.x (default)
- **Umbraco.Cms.Imaging.ImageSharp2** - Uses ImageSharp 2.x for backwards compatibility
**Key difference**: ImageSharp 3.x WebP encoder defaults to Lossless (10x larger files), so this package explicitly sets `WebpFileFormatType.Lossy` at `ConfigureImageSharpMiddlewareOptions.cs:108-115`.
---
## 2. Key Patterns
### Query String Image Processing
Images are processed via URL query parameters handled by ImageSharp.Web middleware:
| Parameter | Purpose | Example |
|-----------|---------|---------|
| `width` / `height` | Resize dimensions | `?width=800&height=600` |
| `mode` | Crop mode (pad, crop, stretch, etc.) | `?mode=crop` |
| `anchor` | Crop anchor position | `?anchor=center` |
| `cc` | Crop coordinates (custom) | `?cc=0.1,0.1,0.1,0.1` |
| `rxy` | Focal point | `?rxy=0.5,0.3` |
| `format` | Output format | `?format=webp` |
| `quality` | Compression quality | `?quality=80` |
### Pipeline Integration
ImageSharp middleware runs **before** static files in `UmbracoBuilderExtensions.cs:44-50`:
```csharp
options.AddFilter(new UmbracoPipelineFilter(nameof(ImageSharpComposer))
{
PrePipeline = prePipeline => prePipeline.UseImageSharp()
});
```
This ensures query strings are processed before serving static files.
### EXIF Orientation Handling
Both `ImageSharpDimensionExtractor` and `CropWebProcessor` account for EXIF rotation:
```csharp
// ImageSharpDimensionExtractor.cs:42-44 - Swap width/height for rotated images
size = IsExifOrientationRotated(imageInfo)
? new Size(imageInfo.Height, imageInfo.Width)
: new Size(imageInfo.Width, imageInfo.Height);
```
```csharp
// CropWebProcessor.cs:64-65 - Transform crop coordinates for EXIF orientation
Vector2 xy1 = ExifOrientationUtilities.Transform(new Vector2(left, top), Vector2.Zero, Vector2.One, orientation);
```
### HMAC Request Authorization
When `HMACSecretKey` is configured, URLs are signed to prevent abuse (`ImageSharpImageUrlGenerator.cs:121-131`):
```csharp
if (_options.HMACSecretKey.Length != 0 && _requestAuthorizationUtilities is not null)
{
var token = _requestAuthorizationUtilities.ComputeHMAC(uri, CommandHandling.Sanitize);
queryString.Add(RequestAuthorizationUtilities.TokenCommand, token);
}
```
---
## 3. Configuration
### ImagingSettings (appsettings.json)
```json
{
"Umbraco": {
"CMS": {
"Imaging": {
"HMACSecretKey": "",
"Cache": {
"BrowserMaxAge": "7.00:00:00",
"CacheMaxAge": "365.00:00:00",
"CacheHashLength": 12,
"CacheFolder": "~/umbraco/Data/TEMP/MediaCache",
"CacheFolderDepth": 8
},
"Resize": {
"MaxWidth": 5000,
"MaxHeight": 5000
}
}
}
}
}
```
### Security: Size Limits Without HMAC
When HMAC is not configured, `ConfigureImageSharpMiddlewareOptions.cs:46-83` enforces max dimensions:
- Width/height requests exceeding `MaxWidth`/`MaxHeight` are **stripped** from the query
- This prevents DoS via excessive image generation
When HMAC **is** configured, size validation is skipped (trusted requests).
### Cache Busting
Query parameters `rnd` or `v` trigger immutable cache headers (`ConfigureImageSharpMiddlewareOptions.cs:86-106`):
- Disables `MustRevalidate`
- Adds `immutable` directive
---
## 4. Core Interfaces Implemented
| Interface | Implementation | Purpose |
|-----------|----------------|---------|
| `IImageDimensionExtractor` | `ImageSharpDimensionExtractor` | Extract width/height from streams |
| `IImageUrlGenerator` | `ImageSharpImageUrlGenerator` | Generate manipulation URLs |
| `IImageWebProcessor` | `CropWebProcessor` | Custom crop with `cc` parameter |
---
## 5. Edge Cases
### WebP Encoding Change (ImageSharp 3.x)
`ConfigureImageSharpMiddlewareOptions.cs:108-115` - ImageSharp 3.x defaults WebP to Lossless for PNGs, creating ~10x larger files. This is overridden:
```csharp
options.Configuration.ImageFormatsManager.SetEncoder(
WebpFormat.Instance,
new WebpEncoder { FileFormat = WebpFileFormatType.Lossy });
```
### Crop Coordinates Format
`CropWebProcessor.cs:50-56` - The `cc` parameter expects 4 values as distances from edges:
- Format: `left,top,right,bottom` (0-1 range, percentages)
- Right/bottom values are **distance from** those edges, not coordinates
- Zero values (`0,0,0,0`) are ignored (no crop)
### Supported File Types
Dynamically determined from ImageSharp configuration (`ImageSharpDimensionExtractor.cs:24`):
```csharp
SupportedImageFileTypes = configuration.ImageFormats.SelectMany(f => f.FileExtensions).ToArray();
```
Default includes: jpg, jpeg, png, gif, bmp, webp, tiff, etc.
---
## Quick Reference
### Essential Commands
```bash
# Build
dotnet build src/Umbraco.Cms.Imaging.ImageSharp/Umbraco.Cms.Imaging.ImageSharp.csproj
# Run tests
dotnet test tests/Umbraco.Tests.UnitTests/ --filter "FullyQualifiedName~ImageSharp"
```
### Key Files
| File | Purpose |
|------|---------|
| `UmbracoBuilderExtensions.cs` | DI registration and pipeline setup |
| `ConfigureImageSharpMiddlewareOptions.cs` | Middleware config (caching, HMAC, size limits) |
| `CropWebProcessor.cs` | Custom `cc` crop parameter |
| `ImageSharpImageUrlGenerator.cs` | URL generation with HMAC signing |
### URL Examples
```
# Basic resize
/media/image.jpg?width=800
# Crop to aspect ratio with focal point
/media/image.jpg?width=800&height=600&mode=crop&rxy=0.5,0.3
# Custom crop coordinates (10% from each edge)
/media/image.jpg?cc=0.1,0.1,0.1,0.1
# Format conversion with quality
/media/image.jpg?format=webp&quality=80
# Cache busted (immutable headers)
/media/image.jpg?width=800&v=abc123
```
### Getting Help
- **Root Documentation**: `/CLAUDE.md`
- **ImageSharp Docs**: https://docs.sixlabors.com/
- **Umbraco Imaging**: https://docs.umbraco.com/umbraco-cms/reference/configuration/imagingsettings
---
**This library provides query string-based image processing. Key concerns are EXIF orientation handling, WebP encoding defaults, and HMAC security for public-facing sites.**

View File

@@ -0,0 +1,139 @@
# Umbraco.Cms.Imaging.ImageSharp2
Image processing library using **ImageSharp 2.x** for backwards compatibility with existing deployments. Use this package only when migrating from older Umbraco versions that depend on ImageSharp 2.x behavior.
**Namespace Note**: Uses `Umbraco.Cms.Imaging.ImageSharp` (same as v3 package) for drop-in replacement - no code changes needed when switching.
---
## 1. Architecture
**Type**: Class Library (NuGet Package)
**Target Framework**: .NET 10.0
**Purpose**: ImageSharp 2.x compatibility layer
### Package Versions (Pinned)
```xml
<!-- From csproj lines 7-8 -->
<PackageReference Include="SixLabors.ImageSharp" VersionOverride="[2.1.11, 3)" />
<PackageReference Include="SixLabors.ImageSharp.Web" VersionOverride="[2.0.2, 3)" />
```
Version constraint `[2.1.11, 3)` means: minimum 2.1.11, below 3.0.
### Project Structure (7 source files)
```
Umbraco.Cms.Imaging.ImageSharp2/
├── ImageSharpComposer.cs # Auto-registration via IComposer
├── UmbracoBuilderExtensions.cs # DI setup and middleware configuration
├── ConfigureImageSharpMiddlewareOptions.cs # Middleware options (caching, size limits)
├── ConfigurePhysicalFileSystemCacheOptions.cs # File cache location
├── ImageProcessors/
│ └── CropWebProcessor.cs # Custom crop processor with EXIF awareness
└── Media/
├── ImageSharpDimensionExtractor.cs # Extract image dimensions (EXIF-aware)
└── ImageSharpImageUrlGenerator.cs # Generate query string URLs
```
---
## 2. Key Differences from ImageSharp (3.x)
| Feature | ImageSharp2 (this) | ImageSharp (3.x) |
|---------|-------------------|------------------|
| **Package version** | 2.1.11 - 2.x | 3.x+ |
| **HMAC signing** | Not supported | Supported |
| **WebP default** | Lossy (native) | Lossless (overridden to Lossy) |
| **Cache buster param** | `rnd` only | `rnd` or `v` |
| **API differences** | `Image.Identify(config, stream)` | `Image.Identify(options, stream)` |
| **Size property** | `image.Image.Size()` method | `image.Image.Size` property |
### API Differences in Code
**ImageSharpDimensionExtractor** (`Media/ImageSharpDimensionExtractor.cs:31`):
```csharp
// v2: Direct method call
IImageInfo imageInfo = Image.Identify(_configuration, stream);
// v3: Uses DecoderOptions
ImageInfo imageInfo = Image.Identify(options, stream);
```
**CropWebProcessor** (`ImageProcessors/CropWebProcessor.cs:67`):
```csharp
// v2: Size is a method
Size size = image.Image.Size();
// v3: Size is a property
Size size = image.Image.Size;
```
### Missing Features (vs ImageSharp 3.x)
1. **No HMAC request authorization** - `HMACSecretKey` setting is ignored
2. **No `v` cache buster** - Only `rnd` parameter triggers immutable headers
3. **No WebP encoder override** - Uses default Lossy encoding (no configuration needed)
---
## 3. When to Use This Package
**Use ImageSharp2 when:**
- Migrating from Umbraco versions that used ImageSharp 2.x
- Third-party packages have hard dependency on ImageSharp 2.x
- Need exact byte-for-byte output compatibility with existing cached images
**Use ImageSharp (3.x) when:**
- New installations
- Need HMAC URL signing for security
- Want latest performance improvements
---
## 4. Configuration
Same as ImageSharp 3.x. See `/src/Umbraco.Cms.Imaging.ImageSharp/CLAUDE.md` → Section 3 for full configuration details.
**Key difference**: `HMACSecretKey` setting exists but is **ignored** in this package (no HMAC support in v2).
---
## Quick Reference
### Essential Commands
```bash
# Build
dotnet build src/Umbraco.Cms.Imaging.ImageSharp2/Umbraco.Cms.Imaging.ImageSharp2.csproj
# Run tests
dotnet test tests/Umbraco.Tests.UnitTests/ --filter "FullyQualifiedName~ImageSharp"
```
### Key Files
| File | Purpose |
|------|---------|
| `Umbraco.Cms.Imaging.ImageSharp2.csproj` | Version constraints (lines 7-8) |
| `ConfigureImageSharpMiddlewareOptions.cs` | Size limit enforcement |
| `Media/ImageSharpImageUrlGenerator.cs` | URL generation (no HMAC) |
### Switching Between Packages
To switch from ImageSharp2 to ImageSharp (3.x):
1. Remove `Umbraco.Cms.Imaging.ImageSharp2` package reference
2. Add `Umbraco.Cms.Imaging.ImageSharp` package reference
3. Clear media cache folder (`~/umbraco/Data/TEMP/MediaCache`)
4. No code changes needed (same namespace)
### Getting Help
- **ImageSharp 3.x Documentation**: `/src/Umbraco.Cms.Imaging.ImageSharp/CLAUDE.md`
- **Root Documentation**: `/CLAUDE.md`
- **SixLabors ImageSharp 2.x Docs**: https://docs.sixlabors.com/
---
**This is a backwards-compatibility package. For new projects, use `Umbraco.Cms.Imaging.ImageSharp` (3.x) instead.**

View File

@@ -0,0 +1,132 @@
# Umbraco.Cms.Persistence.EFCore.SqlServer
SQL Server-specific EF Core provider for Umbraco CMS. Contains SQL Server migrations and provider setup for the EF Core persistence layer.
**Project Type**: Class Library (NuGet package)
**Target Framework**: net10.0
**Dependencies**: Umbraco.Cms.Persistence.EFCore
---
## 1. Architecture
### Project Purpose
This is a thin provider project that implements SQL Server-specific functionality for the EF Core persistence layer:
1. **Migration Provider** - Executes SQL Server-specific migrations
2. **Migration Provider Setup** - Configures DbContext to use SQL Server
3. **Migrations** - SQL Server-specific migration files for OpenIddict tables
### Folder Structure
```
Umbraco.Cms.Persistence.EFCore.SqlServer/
├── Migrations/
│ ├── 20230622184303_InitialCreate.cs # No-op (NPoco creates tables)
│ ├── 20230807654321_AddOpenIddict.cs # OpenIddict tables
│ ├── 20240403140654_UpdateOpenIddictToV5.cs # OpenIddict v5 schema changes
│ ├── 20251006140751_UpdateOpenIddictToV7.cs # Token Type column expansion
│ └── UmbracoDbContextModelSnapshot.cs # Current model state
├── EFCoreSqlServerComposer.cs # DI registration
├── SqlServerMigrationProvider.cs # IMigrationProvider impl
└── SqlServerMigrationProviderSetup.cs # IMigrationProviderSetup impl
```
### Relationship with Parent Project
This project extends `Umbraco.Cms.Persistence.EFCore`:
- Implements `IMigrationProvider` interface defined in parent
- Implements `IMigrationProviderSetup` interface defined in parent
- Uses `UmbracoDbContext` from parent project
- Provider name: `Microsoft.Data.SqlClient` (from `Constants.ProviderNames.SQLServer`)
---
## 2. Commands
**For Git workflow and build commands**, see [repository root](../../CLAUDE.md).
**For EF Core migration commands**, see [parent project](../Umbraco.Cms.Persistence.EFCore/CLAUDE.md) (substitute `-p src/Umbraco.Cms.Persistence.EFCore.SqlServer`).
---
## 3. Key Components
### EFCoreSqlServerComposer (line 10-14)
Registers SQL Server implementations of `IMigrationProvider` and `IMigrationProviderSetup`.
### SqlServerMigrationProvider
Executes migrations via `MigrateAsync(EFCoreMigration)` or `MigrateAllAsync()`. Unlike SQLite sibling, no transaction check needed (SQL Server handles concurrent migrations natively).
### SqlServerMigrationProviderSetup (line 11-14)
Configures `DbContextOptionsBuilder` with `UseSqlServer` and migrations assembly.
---
## 4. Migrations
### Migration History
| Migration | Date | Purpose |
|-----------|------|---------|
| `InitialCreate` | 2023-06-22 | No-op - NPoco creates base tables |
| `AddOpenIddict` | 2023-08-07 | Creates OpenIddict tables |
| `UpdateOpenIddictToV5` | 2024-04-03 | Renames Type→ClientType, adds ApplicationType/JsonWebKeySet/Settings |
| `UpdateOpenIddictToV7` | 2025-10-06 | Expands Token.Type from nvarchar(50) to nvarchar(150) |
### OpenIddict Tables Created
Prefixed with `umbraco`: Applications, Authorizations, Scopes, Tokens (see SQLite sibling for details).
### SQL Server-Specific Differences from SQLite
1. **nvarchar types** - Uses `nvarchar(n)` and `nvarchar(max)` instead of TEXT
2. **v5 migration has actual changes** - Column rename and additions (SQLite handled differently)
3. **v7 migration has schema change** - Token.Type column expanded (SQLite didn't need this)
### Adding New Migrations
1. Configure SQL Server connection string in `src/Umbraco.Web.UI/appsettings.json`
2. Run migration command from repository root (see parent project docs)
3. **Critical**: Also add equivalent migration to `Umbraco.Cms.Persistence.EFCore.Sqlite`
4. Update `SqlServerMigrationProvider.GetMigrationType()` switch (line 27-35) if adding named migrations
---
## 5. Project-Specific Notes
### Named Migration Mapping
`SqlServerMigrationProvider.GetMigrationType()` (line 27-35) maps `EFCoreMigration` enum to migration types. Update both parent enum and this switch when adding named migrations.
### Transaction Handling
SQL Server handles concurrent migrations natively - no transaction check needed (unlike SQLite).
### Auto-Generated Files
`UmbracoDbContextModelSnapshot.cs` is regenerated by EF Core - do not edit manually.
---
## Quick Reference
### Essential Files
| File | Purpose |
|------|---------|
| `SqlServerMigrationProvider.cs` | Migration execution |
| `SqlServerMigrationProviderSetup.cs` | DbContext configuration |
| `EFCoreSqlServerComposer.cs` | DI registration |
| `Migrations/*.cs` | Migration files |
### Provider Name
`Constants.ProviderNames.SQLServer` = `"Microsoft.Data.SqlClient"`
### Related Projects
- **Parent**: `Umbraco.Cms.Persistence.EFCore` - Interfaces and base DbContext
- **Sibling**: `Umbraco.Cms.Persistence.EFCore.Sqlite` - SQLite equivalent

View File

@@ -0,0 +1,134 @@
# Umbraco.Cms.Persistence.EFCore.Sqlite
SQLite-specific EF Core provider for Umbraco CMS. Contains SQLite migrations and provider setup for the EF Core persistence layer.
**Project Type**: Class Library (NuGet package)
**Target Framework**: net10.0
**Dependencies**: Umbraco.Cms.Persistence.EFCore
---
## 1. Architecture
### Project Purpose
This is a thin provider project that implements SQLite-specific functionality for the EF Core persistence layer:
1. **Migration Provider** - Executes SQLite-specific migrations
2. **Migration Provider Setup** - Configures DbContext to use SQLite
3. **Migrations** - SQLite-specific migration files for OpenIddict tables
### Folder Structure
```
Umbraco.Cms.Persistence.EFCore.Sqlite/
├── Migrations/
│ ├── 20230622183638_InitialCreate.cs # No-op (NPoco creates tables)
│ ├── 20230807123456_AddOpenIddict.cs # OpenIddict tables
│ ├── 20240403141051_UpdateOpenIddictToV5.cs # OpenIddict v5 schema
│ ├── 20251006140958_UpdateOpenIddictToV7.cs # OpenIddict v7 (no-op for SQLite)
│ └── UmbracoDbContextModelSnapshot.cs # Current model state
├── EFCoreSqliteComposer.cs # DI registration
├── SqliteMigrationProvider.cs # IMigrationProvider impl
└── SqliteMigrationProviderSetup.cs # IMigrationProviderSetup impl
```
### Relationship with Parent Project
This project extends `Umbraco.Cms.Persistence.EFCore`:
- Implements `IMigrationProvider` interface defined in parent
- Implements `IMigrationProviderSetup` interface defined in parent
- Uses `UmbracoDbContext` from parent project
- Provider name: `Microsoft.Data.Sqlite` (from `Constants.ProviderNames.SQLLite`)
---
## 2. Commands
**For Git workflow and build commands**, see [repository root](../../CLAUDE.md).
**For EF Core migration commands**, see [parent project](../Umbraco.Cms.Persistence.EFCore/CLAUDE.md) (substitute `-p src/Umbraco.Cms.Persistence.EFCore.Sqlite`).
---
## 3. Key Components
### EFCoreSqliteComposer (line 10-14)
Registers `IMigrationProvider` and `IMigrationProviderSetup` for SQLite.
### SqliteMigrationProvider
- `MigrateAsync(EFCoreMigration)` - Runs specific named migration
- `MigrateAllAsync()` - Runs all pending migrations
- **Critical**: Cannot run `MigrateAllAsync` when transaction is active (line 26-29)
### SqliteMigrationProviderSetup (line 11-14)
Configures `DbContextOptionsBuilder` with `UseSqlite` and migrations assembly.
---
## 4. Migrations
### Migration History
| Migration | Date | Purpose |
|-----------|------|---------|
| `InitialCreate` | 2023-06-22 | No-op - NPoco creates base tables |
| `AddOpenIddict` | 2023-08-07 | Creates OpenIddict tables (Applications, Tokens, Authorizations, Scopes) |
| `UpdateOpenIddictToV5` | 2024-04-03 | Schema updates for OpenIddict v5 |
| `UpdateOpenIddictToV7` | 2025-10-06 | No-op for SQLite (no schema changes needed) |
### OpenIddict Tables Created
All tables prefixed with `umbraco`:
- `umbracoOpenIddictApplications` - OAuth client applications
- `umbracoOpenIddictAuthorizations` - User authorizations
- `umbracoOpenIddictScopes` - OAuth scopes
- `umbracoOpenIddictTokens` - Access/refresh tokens
### SQLite-Specific Notes
- **TEXT column type** - SQLite uses TEXT for all strings (no varchar)
- **InitialCreate is no-op** - NPoco creates base tables, EF Core manages OpenIddict only
- **v7 migration is no-op** - SQLite didn't require schema changes that SQL Server needed
### Adding New Migrations
1. Configure SQLite connection string in `src/Umbraco.Web.UI/appsettings.json`
2. Run migration command from repository root (see parent project docs)
3. **Critical**: Also add equivalent migration to `Umbraco.Cms.Persistence.EFCore.SqlServer`
4. Update `SqliteMigrationProvider.GetMigrationType()` switch (line 34-42) if adding named migrations
---
## 5. Project-Specific Notes
### Named Migration Mapping
`SqliteMigrationProvider.GetMigrationType()` (line 34-42) maps `EFCoreMigration` enum to migration types. When adding named migrations, update both the parent project's enum and this switch.
### Auto-Generated Files
`UmbracoDbContextModelSnapshot.cs` is regenerated by EF Core - do not edit manually.
---
## Quick Reference
### Essential Files
| File | Purpose |
|------|---------|
| `SqliteMigrationProvider.cs` | Migration execution |
| `SqliteMigrationProviderSetup.cs` | DbContext configuration |
| `EFCoreSqliteComposer.cs` | DI registration |
| `Migrations/*.cs` | Migration files |
### Provider Name
`Constants.ProviderNames.SQLLite` = `"Microsoft.Data.Sqlite"`
### Related Projects
- **Parent**: `Umbraco.Cms.Persistence.EFCore` - Interfaces and base DbContext
- **Sibling**: `Umbraco.Cms.Persistence.EFCore.SqlServer` - SQL Server equivalent

View File

@@ -0,0 +1,255 @@
# Umbraco.Cms.Persistence.EFCore
Entity Framework Core persistence layer for Umbraco CMS. This project provides EF Core integration including scoping, distributed locking, and migration infrastructure.
**Project Type**: Class Library (NuGet package)
**Target Framework**: net10.0
**Dependencies**: Umbraco.Core, Umbraco.Infrastructure
---
## 1. Architecture
### Project Purpose
This project bridges EF Core with Umbraco's existing persistence infrastructure. It provides:
1. **UmbracoDbContext** - Base DbContext with automatic table prefix (`umbraco`) and provider-based configuration
2. **EF Core Scoping** - Integration with Umbraco's Unit of Work pattern (scopes)
3. **Distributed Locking** - SQL Server and SQLite locking mechanisms for EF Core contexts
4. **Migration Infrastructure** - Provider-agnostic migration execution
### Folder Structure
```
Umbraco.Cms.Persistence.EFCore/
├── Composition/
│ └── UmbracoEFCoreComposer.cs # DI registration, OpenIddict setup
├── Extensions/
│ ├── DbContextExtensions.cs # ExecuteScalarAsync, MigrateDatabaseAsync
│ └── UmbracoEFCoreServiceCollectionExtensions.cs # AddUmbracoDbContext<T>
├── Locking/
│ ├── SqlServerEFCoreDistributedLockingMechanism.cs
│ └── SqliteEFCoreDistributedLockingMechanism.cs
├── Migrations/
│ ├── IMigrationProvider.cs # Provider-specific migration execution
│ └── IMigrationProviderSetup.cs # DbContext options setup per provider
├── Scoping/
│ ├── AmbientEFCoreScopeStack.cs # AsyncLocal scope stack
│ ├── EFCoreScope.cs # Main scope implementation
│ ├── EFCoreDetachableScope.cs # Detachable scope for Deploy
│ ├── EFCoreScopeProvider.cs # Scope factory
│ ├── EFCoreScopeAccessor.cs # Ambient scope accessor
│ └── I*.cs # Interfaces
├── Constants-ProviderNames.cs # SQLite/SQLServer provider constants
├── EfCoreMigrationExecutor.cs # Migration orchestrator
├── StringExtensions.cs # Provider name comparison
└── UmbracoDbContext.cs # Base DbContext
```
### Key Design Decisions
1. **Generic DbContext Support** - All scoping/locking is generic (`<TDbContext>`) allowing custom contexts
2. **Parallel NPoco Coexistence** - Designed to work alongside existing NPoco persistence layer
3. **Provider Abstraction** - `IMigrationProvider` and `IMigrationProviderSetup` enable database-agnostic migrations
4. **OpenIddict Integration** - UmbracoDbContext automatically registers OpenIddict entity sets
### Database Providers
Provider-specific implementations live in separate projects:
- `Umbraco.Cms.Persistence.EFCore.SqlServer` - SQL Server provider and migrations
- `Umbraco.Cms.Persistence.EFCore.Sqlite` - SQLite provider and migrations
Provider names (from `Constants.ProviderNames`):
- `Microsoft.Data.SqlClient` - SQL Server
- `Microsoft.Data.Sqlite` - SQLite
---
## 2. Commands
**For Git workflow and standard build commands**, see [repository root](../../CLAUDE.md).
### EF Core Migrations
**Important**: Run from repository root with valid connection string in `src/Umbraco.Web.UI/appsettings.json`.
```bash
# Add migration (SQL Server)
dotnet ef migrations add %MigrationName% -s src/Umbraco.Web.UI -p src/Umbraco.Cms.Persistence.EFCore.SqlServer -c UmbracoDbContext
# Add migration (SQLite)
dotnet ef migrations add %MigrationName% -s src/Umbraco.Web.UI -p src/Umbraco.Cms.Persistence.EFCore.Sqlite -c UmbracoDbContext
# Remove last migration (SQL Server)
dotnet ef migrations remove -s src/Umbraco.Web.UI -p src/Umbraco.Cms.Persistence.EFCore.SqlServer
# Remove last migration (SQLite)
dotnet ef migrations remove -s src/Umbraco.Web.UI -p src/Umbraco.Cms.Persistence.EFCore.Sqlite
# Generate migration script
dotnet ef migrations script -s src/Umbraco.Web.UI -p src/Umbraco.Cms.Persistence.EFCore.SqlServer
```
---
## 3. Key Patterns
### Adding a Custom DbContext
Use `AddUmbracoDbContext<T>` to register with full scope integration:
```csharp
services.AddUmbracoDbContext<MyCustomDbContext>((provider, options, connectionString, providerName) =>
{
options.UseOpenIddict(); // Optional
});
```
Registers: `IDbContextFactory<T>`, scope providers, accessors, and distributed locking
### Using EF Core Scopes
```csharp
using IEfCoreScope<UmbracoDbContext> scope = _scopeProvider.CreateScope();
await scope.ExecuteWithContextAsync<MyEntity>(async dbContext =>
{
return await dbContext.MyEntities.FirstOrDefaultAsync(x => x.Id == id);
});
scope.Complete(); // Required to commit
```
### UmbracoDbContext Table Naming
All tables auto-prefixed with `umbraco` (see `UmbracoDbContext.cs:85-88`).
---
## 4. Scoping System
### Scope Hierarchy
```
EFCoreScopeProvider<T>
├── Creates EFCoreScope<T> (normal scope)
└── Creates EFCoreDetachableScope<T> (for Deploy scenarios)
EFCoreScope<T>
├── Manages DbContext lifecycle
├── Handles transactions (BeginTransaction/Commit/Rollback)
├── Integrates with parent NPoco IScope when nested
└── Manages distributed locks via Locks property
```
### Ambient Scope Stack
Uses `AsyncLocal<ConcurrentStack<T>>` for thread-safe tracking. Scopes must be disposed in LIFO order (child before parent).
### Transaction Integration
When an EF Core scope is created inside an NPoco scope:
1. The EF Core scope reuses the parent's `DbTransaction`
2. Transaction commit/rollback is delegated to the parent scope
3. DbContext connects to the same connection as the parent
See `EFCoreScope.cs:158-180` for transaction initialization logic.
---
## 5. Distributed Locking
### SQL Server Locking
Table-level locks with `REPEATABLEREAD` hint. Timeout via `SET LOCK_TIMEOUT`. Requires `ReadCommitted` or higher.
### SQLite Locking
Database-level locking (SQLite limitation). Read locks use snapshot isolation (WAL mode). Write locks are exclusive. Handles `SQLITE_BUSY/LOCKED` errors.
### Lock Requirements
Requires active scope, transaction, and minimum `ReadCommitted` isolation.
---
## 6. Migration System
### How Migrations Execute
1. `UmbracoEFCoreComposer` registers `EfCoreMigrationExecutor`
2. Notification handler triggers on database creation
3. `EfCoreMigrationExecutor` finds correct `IMigrationProvider` by provider name
4. Provider executes via `dbContext.Database.Migrate()`
### Adding New Migrations
Create equivalent migrations in both SqlServer and Sqlite provider projects using commands from section 2.
---
## 7. Project-Specific Notes
### Known Technical Debt
**Warning Suppressions** (`.csproj:9-14`): `IDE0270` (null checks), `CS0108` (hiding members), `CS1998` (async/await).
**Detachable Scope** (`EFCoreDetachableScope.cs:93-94`): TODO for Deploy integration, limited test coverage.
### Circular Dependency Handling
`SqlServerEFCoreDistributedLockingMechanism` uses `Lazy<IEFCoreScopeAccessor<T>>` to break circular dependency.
### StaticServiceProvider Usage
Fallback for design-time EF tooling (migrations) and startup edge cases when DI unavailable.
### OpenIddict Integration
`UmbracoEFCoreComposer` configures OpenIddict via `options.UseOpenIddict()` (see `Composition/UmbracoEFCoreComposer.cs:22-36`).
### Vulnerable Dependency Override
Top-level dependency on `Microsoft.Extensions.Caching.Memory` overrides vulnerable EF Core transitive dependency (`.csproj:18-19`).
### InternalsVisibleTo
`Umbraco.Tests.Integration` has access to internal types.
---
## Quick Reference
### Essential Files
| File | Purpose |
|------|---------|
| `UmbracoDbContext.cs` | Base DbContext with table prefixing |
| `EFCoreScope.cs` | Main scope implementation |
| `EFCoreScopeProvider.cs` | Scope factory |
| `UmbracoEFCoreServiceCollectionExtensions.cs` | `AddUmbracoDbContext<T>` extension |
| `UmbracoEFCoreComposer.cs` | DI registration |
### Key Interfaces
| Interface | Purpose |
|-----------|---------|
| `IEfCoreScope<TDbContext>` | Scope contract with `ExecuteWithContextAsync` |
| `IEFCoreScopeProvider<TDbContext>` | Creates/manages scopes |
| `IEFCoreScopeAccessor<TDbContext>` | Access ambient scope |
| `IMigrationProvider` | Provider-specific migration execution |
| `IMigrationProviderSetup` | DbContext options setup per provider |
### Provider Constants
```csharp
Constants.ProviderNames.SQLServer = "Microsoft.Data.SqlClient"
Constants.ProviderNames.SQLLite = "Microsoft.Data.Sqlite"
```
### Related Projects
- `Umbraco.Cms.Persistence.EFCore.SqlServer` - SQL Server implementation
- `Umbraco.Cms.Persistence.EFCore.Sqlite` - SQLite implementation
- `Umbraco.Infrastructure` - Contains NPoco scoping that this integrates with

View File

@@ -0,0 +1,235 @@
# Umbraco.Cms.Persistence.SqlServer
SQL Server database provider for Umbraco CMS using NPoco ORM. Provides SQL Server-specific SQL syntax, bulk copy operations, distributed locking, and Azure SQL transient fault handling.
**Project Type**: Class Library (NuGet package)
**Target Framework**: net10.0
**Dependencies**: Umbraco.Infrastructure, NPoco.SqlServer, Microsoft.Data.SqlClient
**Note**: This is the **legacy NPoco-based** SQL Server provider. For EF Core SQL Server support, see `Umbraco.Cms.Persistence.EFCore.SqlServer`.
---
## 1. Architecture
### Project Purpose
This project provides complete SQL Server support for Umbraco's NPoco-based persistence layer:
1. **SQL Syntax Provider** - SQL Server-specific SQL generation and schema operations
2. **Bulk Insert** - True `SqlBulkCopy` for high-performance batch inserts
3. **Distributed Locking** - Row-level locking with `REPEATABLEREAD` hints
4. **Fault Handling** - Azure SQL and network transient error retry policies
5. **LocalDB Support** - SQL Server LocalDB for development
### Folder Structure
```
Umbraco.Cms.Persistence.SqlServer/
├── Dtos/
│ ├── ColumnInSchemaDto.cs # Schema query results
│ ├── ConstraintPerColumnDto.cs
│ ├── ConstraintPerTableDto.cs
│ ├── DefaultConstraintPerColumnDto.cs
│ └── DefinedIndexDto.cs
├── FaultHandling/
│ ├── RetryPolicyFactory.cs # Creates retry policies
│ └── Strategies/
│ ├── NetworkConnectivityErrorDetectionStrategy.cs
│ ├── SqlAzureTransientErrorDetectionStrategy.cs # Azure SQL error codes
│ └── ThrottlingCondition.cs # Azure throttling decode
├── Interceptors/
│ ├── SqlServerAddMiniProfilerInterceptor.cs
│ ├── SqlServerAddRetryPolicyInterceptor.cs
│ └── SqlServerConnectionInterceptor.cs # Base interceptor
├── Services/
│ ├── BulkDataReader.cs # Base bulk reader
│ ├── MicrosoftSqlSyntaxProviderBase.cs # Shared SQL syntax
│ ├── PocoDataDataReader.cs # NPoco bulk reader
│ ├── SqlServerBulkSqlInsertProvider.cs # SqlBulkCopy wrapper
│ ├── SqlServerDatabaseCreator.cs
│ ├── SqlServerDistributedLockingMechanism.cs
│ ├── SqlServerSpecificMapperFactory.cs
│ ├── SqlServerSyntaxProvider.cs # Main syntax provider
│ ├── SqlAzureDatabaseProviderMetadata.cs
│ ├── SqlLocalDbDatabaseProviderMetadata.cs
│ └── SqlServerDatabaseProviderMetadata.cs
├── Constants.cs # Provider name
├── LocalDb.cs # LocalDB management (~1,100 lines)
├── NPocoSqlServerDatabaseExtensions.cs # NPoco bulk extensions config
├── SqlServerComposer.cs # DI auto-registration
└── UmbracoBuilderExtensions.cs # AddUmbracoSqlServerSupport()
```
### Auto-Registration
When this assembly is referenced, `SqlServerComposer` automatically registers all SQL Server services via `AddUmbracoSqlServerSupport()`.
### Provider Name Migration
Auto-migrates legacy `System.Data.SqlClient` to `Microsoft.Data.SqlClient`:
**Line 53-59**: `UmbracoBuilderExtensions.cs` updates connection string provider name
```csharp
if (options.ProviderName == "System.Data.SqlClient")
{
options.ProviderName = Constants.ProviderName; // Microsoft.Data.SqlClient
}
```
---
## 2. Commands
**For Git workflow and build commands**, see [repository root](../../CLAUDE.md).
---
## 3. Key Components
### SqlServerDistributedLockingMechanism (Services/SqlServerDistributedLockingMechanism.cs)
Row-level locking using `REPEATABLEREAD` table hints:
- **Read locks** (line 147): `SELECT value FROM umbracoLock WITH (REPEATABLEREAD) WHERE id=@id`
- **Write locks** (line 182-183): `UPDATE umbracoLock WITH (REPEATABLEREAD) SET value = ... WHERE id=@id`
- **Timeout**: Uses `SET LOCK_TIMEOUT {milliseconds}` (lines 149, 185)
- **Error handling**: Catches SQL error 1222 (lock timeout) → throws `DistributedReadLockTimeoutException` or `DistributedWriteLockTimeoutException`
- **Minimum isolation**: Requires `ReadCommitted` or higher (lines 141-145, 176-180)
### SqlServerBulkSqlInsertProvider (Services/SqlServerBulkSqlInsertProvider.cs)
True bulk insert using `SqlBulkCopy`:
```csharp
// Line 61-68: SqlBulkCopy configuration
new SqlBulkCopy(tConnection, SqlBulkCopyOptions.Default, tTransaction)
{
BulkCopyTimeout = 0, // No timeout (uses connection timeout)
DestinationTableName = tableName,
BatchSize = 4096, // Consistent with NPoco
}
```
**Key features**:
- Streams data via `PocoDataDataReader<T>` (line 69) - avoids in-memory DataTable
- Explicit column mappings by name (lines 74-77) - prevents column order mismatches
- Returns actual count inserted (line 80) - NPoco's `InsertBulk` doesn't provide this
### SqlAzureTransientErrorDetectionStrategy (FaultHandling/Strategies/)
Detects transient Azure SQL errors for retry:
| Error Code | Description |
|------------|-------------|
| 40501 | Service busy (throttling) |
| 10928/10929 | Resource limits reached |
| 10053/10054 | Transport-level errors |
| 10060 | Connection timeout |
| 40197/40540/40143 | Service processing errors |
| 40613 | Database not available |
| 233 | Connection initialization error |
| 64 | Login process error |
### SqlServerSyntaxProvider (Services/SqlServerSyntaxProvider.cs)
SQL Server version detection and syntax generation:
- **Engine editions**: Desktop, Standard, Enterprise, Express, Azure (lines 23-31)
- **SQL Server versions**: V7 through V2019, plus Azure (lines 33-47)
- **Default isolation**: `ReadCommitted` (line 69)
- **Azure detection**: `ServerVersion?.IsAzure` determines `DbProvider` (line 67)
- **Version detection**: Populated via `GetDbProviderManifest()` by querying SQL Server metadata
---
## 4. LocalDB Support
`LocalDb.cs` (~1,100 lines) provides comprehensive SQL Server LocalDB management:
- Database creation/deletion
- Instance management
- File path handling
- Connection string generation
**Known issues** (from TODO comments):
- Line 358: Stale database handling not implemented
- Line 359: File name assumptions may not always be correct
---
## 5. Differences from SQLite Provider
| Feature | SQL Server | SQLite |
|---------|-----------|--------|
| **Bulk Insert** | `SqlBulkCopy` (true bulk) | Transaction + individual inserts |
| **Locking** | Row-level with `REPEATABLEREAD` hints | Database-level (WAL mode) |
| **Types** | Native (NVARCHAR, DECIMAL, UNIQUEIDENTIFIER) | TEXT for most types |
| **Transient Retry** | Azure SQL error codes + network errors | BUSY/LOCKED retry only |
| **LocalDB** | Full support (~1,100 lines) | N/A |
---
## 6. Project-Specific Notes
### InternalsVisibleTo
Test projects have access to internal types:
```xml
<InternalsVisibleTo>Umbraco.Tests.Integration</InternalsVisibleTo>
<InternalsVisibleTo>Umbraco.Tests.UnitTests</InternalsVisibleTo>
```
### Known Technical Debt
1. **Warning Suppressions** (`.csproj:8-22`): Multiple analyzer warnings suppressed
- StyleCop: SA1405, SA1121, SA1117
- IDE: 1006 (naming), 0270/0057/0054/0048 (simplification)
- CS0618 (obsolete usage), CS1574 (XML comments)
2. **BulkInsert TODO** (`SqlServerBulkSqlInsertProvider.cs:44-46`): Custom `SqlBulkCopy` implementation used because NPoco's `InsertBulk` doesn't return record count. Performance comparison vs NPoco's DataTable approach pending.
3. **LocalDB TODOs** (`LocalDb.cs:358-359`): Stale database cleanup and file name assumption handling incomplete.
### NPoco Bulk Extensions
`NPocoSqlServerDatabaseExtensions.ConfigureNPocoBulkExtensions()` is called during registration (line 50) to configure NPoco's SQL Server bulk operations.
### Three Provider Metadata Types
Different metadata for different SQL Server variants:
- `SqlServerDatabaseProviderMetadata` - Standard SQL Server
- `SqlLocalDbDatabaseProviderMetadata` - LocalDB
- `SqlAzureDatabaseProviderMetadata` - Azure SQL
---
## Quick Reference
### Essential Files
| File | Purpose |
|------|---------|
| `SqlServerSyntaxProvider.cs` | Core SQL syntax provider |
| `SqlServerDistributedLockingMechanism.cs` | Row-level locking |
| `SqlServerBulkSqlInsertProvider.cs` | SqlBulkCopy wrapper |
| `SqlAzureTransientErrorDetectionStrategy.cs` | Azure retry logic |
| `LocalDb.cs` | LocalDB management |
### Provider Name
`Constants.ProviderName` = `"Microsoft.Data.SqlClient"`
### Registered Services
All registered via `TryAddEnumerable`:
- `ISqlSyntaxProvider``SqlServerSyntaxProvider`
- `IBulkSqlInsertProvider``SqlServerBulkSqlInsertProvider`
- `IDatabaseCreator``SqlServerDatabaseCreator`
- `IProviderSpecificMapperFactory``SqlServerSpecificMapperFactory`
- `IDatabaseProviderMetadata` → Three variants (SqlServer, LocalDb, Azure)
- `IDistributedLockingMechanism``SqlServerDistributedLockingMechanism`
- `IProviderSpecificInterceptor` → MiniProfiler and RetryPolicy interceptors
### Related Projects
- **Sibling**: `Umbraco.Cms.Persistence.Sqlite` - SQLite NPoco provider
- **EF Core**: `Umbraco.Cms.Persistence.EFCore.SqlServer` - EF Core SQL Server provider

View File

@@ -0,0 +1,204 @@
# Umbraco.Cms.Persistence.Sqlite
SQLite database provider for Umbraco CMS using NPoco ORM. Provides SQLite-specific SQL syntax, type mappers, distributed locking, and connection interceptors.
**Project Type**: Class Library (NuGet package)
**Target Framework**: net10.0
**Dependencies**: Umbraco.Infrastructure, Microsoft.Data.Sqlite
**Note**: This is the **legacy NPoco-based** SQLite provider. For EF Core SQLite support, see `Umbraco.Cms.Persistence.EFCore.Sqlite`.
---
## 1. Architecture
### Project Purpose
This project provides complete SQLite support for Umbraco's NPoco-based persistence layer:
1. **SQL Syntax Provider** - SQLite-specific SQL generation and schema operations
2. **Type Mappers** - GUID, decimal, date/time mapping for SQLite's type system
3. **Distributed Locking** - SQLite-specific locking using WAL mode
4. **Connection Interceptors** - Deferred transactions, MiniProfiler, retry policies
5. **Bulk Insert** - SQLite-optimized record insertion
### Folder Structure
```
Umbraco.Cms.Persistence.Sqlite/
├── Interceptors/
│ ├── SqliteAddMiniProfilerInterceptor.cs # MiniProfiler integration
│ ├── SqliteAddPreferDeferredInterceptor.cs # Deferred transaction wrapper
│ ├── SqliteAddRetryPolicyInterceptor.cs # Transient error retry
│ └── SqliteConnectionInterceptor.cs # Base interceptor
├── Mappers/
│ ├── SqliteGuidScalarMapper.cs # GUID as TEXT mapping
│ ├── SqlitePocoDateAndTimeOnlyMapper.cs # Date/Time as TEXT
│ ├── SqlitePocoDecimalMapper.cs # Decimal as TEXT (lossless)
│ └── SqlitePocoGuidMapper.cs # GUID for NPoco
├── Services/
│ ├── SqliteBulkSqlInsertProvider.cs # Bulk insert via transactions
│ ├── SqliteDatabaseCreator.cs # Database initialization
│ ├── SqliteDatabaseProviderMetadata.cs # Provider info
│ ├── SqliteDistributedLockingMechanism.cs # WAL-based locking
│ ├── SqliteExceptionExtensions.cs # Error code helpers
│ ├── SqlitePreferDeferredTransactionsConnection.cs # Deferred tx wrapper
│ ├── SqliteSpecificMapperFactory.cs # Type mapper factory
│ ├── SqliteSyntaxProvider.cs # SQL syntax (481 lines)
│ └── SqliteTransientErrorDetectionStrategy.cs # BUSY/LOCKED detection
├── Constants.cs # Provider name constant
├── SqliteComposer.cs # DI auto-registration
└── UmbracoBuilderExtensions.cs # AddUmbracoSqliteSupport()
```
### Auto-Registration
When this assembly is referenced, `SqliteComposer` automatically registers all SQLite services via the `AddUmbracoSqliteSupport()` extension method.
---
## 2. Commands
**For Git workflow and build commands**, see [repository root](../../CLAUDE.md).
---
## 3. Key Components
### SqliteSyntaxProvider (Services/SqliteSyntaxProvider.cs)
The core SQL syntax provider implementing `ISqlSyntaxProvider`. Key characteristics:
- **All strings stored as TEXT** with `COLLATE NOCASE` (case-insensitive)
- **GUIDs stored as TEXT** (no native GUID support)
- **Decimals stored as TEXT** to avoid REAL precision loss (line 51)
- **Default isolation level**: `IsolationLevel.Serializable` (line 66-67)
- **No identity insert support** (line 73)
- **No clustered index support** (line 76)
- **LIMIT instead of TOP** for pagination (line 273-278)
- **AUTOINCREMENT required** to prevent magic ID issues with negative IDs (line 224-228)
### SqliteDistributedLockingMechanism (Services/SqliteDistributedLockingMechanism.cs)
Database-level locking using SQLite WAL mode:
- **Read locks**: Snapshot isolation via WAL (mostly no-op, just validates transaction exists)
- **Write locks**: Only one writer at a time (entire database locked)
- **Timeout handling**: Uses `CommandTimeout` for busy-wait (line 163)
- **Error handling**: Catches `SQLITE_BUSY` and `SQLITE_LOCKED` errors
### SqlitePreferDeferredTransactionsConnection (Services/SqlitePreferDeferredTransactionsConnection.cs)
Wraps `SqliteConnection` to force deferred transactions:
```csharp
// Line 33-34: The critical behavior
protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel)
=> _inner.BeginTransaction(isolationLevel, true); // <-- The important bit
```
This prevents immediate write locks when beginning transactions, allowing multiple readers.
### Connection Interceptors
- **SqliteAddPreferDeferredInterceptor** - Wraps connections with deferred transaction support
- **SqliteAddMiniProfilerInterceptor** - Adds profiling for development
- **SqliteAddRetryPolicyInterceptor** - Retries on transient SQLite errors
---
## 4. SQLite Type Mapping
SQLite has limited types. Umbraco maps .NET types as follows:
| .NET Type | SQLite Type | Notes |
|-----------|-------------|-------|
| `int`, `long`, `bool` | INTEGER | Native support |
| `string` | TEXT COLLATE NOCASE | Case-insensitive |
| `Guid` | TEXT | Stored as string representation |
| `DateTime`, `DateTimeOffset` | TEXT | ISO format string |
| `decimal` | TEXT | Prevents REAL precision loss |
| `double`, `float` | REAL | Native support |
| `byte[]` | BLOB | Native support |
---
## 5. Project-Specific Notes
### Database Creation Prevention
`UmbracoBuilderExtensions.cs` (line 49-64) prevents accidental database file creation:
```csharp
// Changes ReadWriteCreate mode to ReadWrite only
if (connectionStringBuilder.Mode == SqliteOpenMode.ReadWriteCreate)
{
connectionStringBuilder.Mode = SqliteOpenMode.ReadWrite;
}
```
This ensures the database must exist before Umbraco connects.
### WAL Mode Locking
SQLite with WAL (Write-Ahead Logging) journal mode:
- Multiple readers can access snapshots concurrently
- Only one writer at a time (database-level lock)
- Write lock uses `umbracoLock` table with UPDATE to acquire
### Bulk Insert Implementation
`SqliteBulkSqlInsertProvider` (line 38-60) doesn't use true bulk copy (SQLite doesn't support it). Instead:
1. Wraps inserts in a transaction if not already in one
2. Inserts records one-by-one with `database.Insert()`
3. Completes transaction
This is slower than SQL Server's `SqlBulkCopy` but safe for SQLite.
### Known Technical Debt
1. **Warning Suppression** (`.csproj:8-12`): `CS0114` - hiding inherited members needs fixing
2. **TODO in SqliteSyntaxProvider** (line 178): `TryGetDefaultConstraint` not implemented for SQLite
### Differences from SQL Server Provider
| Feature | SQLite | SQL Server |
|---------|--------|------------|
| Bulk Insert | Transaction + individual inserts | SqlBulkCopy |
| Locking | Database-level (WAL) | Row-level |
| Identity Insert | Not supported | Supported |
| Clustered Indexes | Not supported | Supported |
| TOP clause | LIMIT | TOP |
| Decimal | TEXT (lossless) | DECIMAL |
| GUID | TEXT | UNIQUEIDENTIFIER |
---
## Quick Reference
### Essential Files
| File | Purpose |
|------|---------|
| `SqliteSyntaxProvider.cs` | Core SQL syntax provider |
| `SqliteDistributedLockingMechanism.cs` | Database locking |
| `UmbracoBuilderExtensions.cs` | DI registration |
| `SqlitePreferDeferredTransactionsConnection.cs` | Deferred tx support |
### Provider Name
`Constants.ProviderName` = `"Microsoft.Data.Sqlite"`
### Registered Services
All registered via `TryAddEnumerable` (allowing multiple providers):
- `ISqlSyntaxProvider``SqliteSyntaxProvider`
- `IBulkSqlInsertProvider``SqliteBulkSqlInsertProvider`
- `IDatabaseCreator``SqliteDatabaseCreator`
- `IProviderSpecificMapperFactory``SqliteSpecificMapperFactory`
- `IDatabaseProviderMetadata``SqliteDatabaseProviderMetadata`
- `IDistributedLockingMechanism``SqliteDistributedLockingMechanism`
- `IProviderSpecificInterceptor` → Three interceptors
### Related Projects
- **Sibling**: `Umbraco.Cms.Persistence.SqlServer` - SQL Server NPoco provider
- **EF Core**: `Umbraco.Cms.Persistence.EFCore.Sqlite` - EF Core SQLite provider

View File

@@ -0,0 +1,260 @@
# Umbraco.Cms.StaticAssets
Static assets and Razor views for Umbraco CMS. This is a Razor Class Library (RCL) that packages all backoffice, login, and website assets for deployment.
**Project Type**: Razor Class Library (NuGet package)
**Target Framework**: net10.0
**SDK**: Microsoft.NET.Sdk.Razor
---
## 1. Architecture
### Project Purpose
This project packages all static assets required for Umbraco CMS at runtime:
1. **Razor Views** - Server-rendered pages for backoffice, login, and error pages
2. **Static Web Assets** - JavaScript, CSS, fonts, images served at runtime
3. **Build Integration** - MSBuild targets that compile TypeScript/frontend projects on demand
### Key Characteristic
**No C# Source Code** - This is a pure asset packaging project. All functionality comes from referenced projects (`Umbraco.Cms.Api.Management`, `Umbraco.Web.Website`).
### Folder Structure
```
Umbraco.Cms.StaticAssets/
├── umbraco/ # Razor Views (server-rendered)
│ ├── UmbracoBackOffice/
│ │ └── Index.cshtml # Backoffice SPA shell (69 lines)
│ ├── UmbracoLogin/
│ │ └── Index.cshtml # Login page (99 lines)
│ └── UmbracoWebsite/
│ ├── NoNodes.cshtml # "No published content" page
│ ├── NotFound.cshtml # 404 error page
│ └── Maintenance.cshtml # Maintenance mode page
├── wwwroot/ # Static Web Assets
│ ├── App_Plugins/
│ │ └── Umbraco.BlockGridEditor.DefaultCustomViews/ # Block grid demo templates
│ └── umbraco/
│ ├── assets/ # Logos and branding (see README.md)
│ ├── backoffice/ # Built backoffice SPA (from Umbraco.Web.UI.Client)
│ │ ├── apps/ # Application modules
│ │ ├── assets/ # Fonts, language files
│ │ ├── css/ # Themes and stylesheets
│ │ └── monaco-editor/ # Code editor assets
│ ├── login/ # Built login SPA (from Umbraco.Web.UI.Login)
│ └── website/ # Frontend website assets (fonts, CSS)
└── Umbraco.Cms.StaticAssets.csproj # Build configuration (149 lines)
```
### Project Dependencies
```xml
<ProjectReference Include="..\Umbraco.Cms.Api.Management\Umbraco.Cms.Api.Management.csproj" />
<ProjectReference Include="..\Umbraco.Web.Website\Umbraco.Web.Website.csproj" />
```
---
## 2. Commands
**For Git workflow and build commands**, see [repository root](../../CLAUDE.md).
---
## 3. Key Components
### Razor Views
| View | Purpose | Line Count |
|------|---------|------------|
| `UmbracoBackOffice/Index.cshtml` | Backoffice SPA entry point with `<umb-app>` web component | 69 |
| `UmbracoLogin/Index.cshtml` | Login page with `<umb-auth>` web component | 99 |
| `UmbracoWebsite/NoNodes.cshtml` | Welcome page when no content published | 59 |
| `UmbracoWebsite/NotFound.cshtml` | 404 error page (debug info in debug mode) | 79 |
| `UmbracoWebsite/Maintenance.cshtml` | Maintenance mode during upgrades | 65 |
### Backoffice Index View (umbraco/UmbracoBackOffice/Index.cshtml)
Key injected services:
- `IBackOfficePathGenerator` - Generates backoffice URL paths
- `IPackageManifestService` - Package manifest discovery
- `IJsonSerializer` - JSON serialization for import maps
- `IProfilerHtml` - MiniProfiler integration
Key features:
- **Import Maps** (line 35): `Html.BackOfficeImportMapScriptAsync()` generates JavaScript module import maps
- **Debug Mode** (line 16, 63-66): `?umbDebug=true` query param enables profiler
- **NoScript Fallback** (lines 40-60): Displays message if JavaScript disabled
### Login View (umbraco/UmbracoLogin/Index.cshtml)
Configures `<umb-auth>` web component with attributes:
- `return-url` - Redirect after login
- `logo-image` / `background-image` - Branding URLs from `BackOfficeGraphicsController`
- `username-is-email` - From `SecuritySettings`
- `allow-user-invite` / `allow-password-reset` - Email capability check
- `disable-local-login` - External login provider configuration
---
## 4. Build System
### Frontend Build Integration (csproj lines 36-90)
The `.csproj` contains MSBuild targets that automatically build frontend projects when assets are missing.
**Backoffice Build** (lines 36-90):
```
BackofficeProjectDirectory = ../Umbraco.Web.UI.Client/
BackofficeAssetsPath = wwwroot/umbraco/backoffice
```
**Login Build** (lines 94-148):
```
LoginProjectDirectory = ../Umbraco.Web.UI.Login/
LoginAssetsPath = wwwroot/umbraco/login
```
### Build Targets
| Target | Purpose |
|--------|---------|
| `BuildStaticAssetsPreconditions` | Checks if build needed (Visual Studio only) |
| `RestoreBackoffice` | Runs `npm i` if package-lock changed |
| `BuildBackoffice` | Runs `npm run build:for:cms` |
| `DefineBackofficeAssets` | Registers assets with StaticWebAssets system |
| `CleanBackoffice` | Removes built assets on `dotnet clean` |
### Build Conditions
- **UmbracoBuild Variable**: When set (CI/CD), skips frontend builds (pre-built assets expected)
- **Visual Studio Detection**: `'$(UmbracoBuild)' == ''` indicates VS build
- **preserve.backoffice Marker**: Skip clean if `preserve.backoffice` file exists in solution root
---
## 5. Static Web Assets
### Asset Categories
| Directory | Contents | Served At |
|-----------|----------|-----------|
| `wwwroot/umbraco/assets/` | Branding logos | Via `BackOfficeGraphicsController` API |
| `wwwroot/umbraco/backoffice/` | Built backoffice SPA | `/umbraco/backoffice/*` |
| `wwwroot/umbraco/login/` | Built login SPA | `/umbraco/login/*` |
| `wwwroot/umbraco/website/` | Website assets (fonts, CSS) | `/umbraco/website/*` |
| `wwwroot/App_Plugins/` | Block editor demo views | `/App_Plugins/*` |
### Logo Assets (wwwroot/umbraco/assets/)
Documented in `wwwroot/umbraco/assets/README.md` (16 lines):
| File | Usage | API Endpoint |
|------|-------|--------------|
| `logo.svg` | Backoffice and public sites | `/umbraco/management/api/v1/security/back-office/graphics/logo` |
| `logo_dark.svg` | Login screen (dark mode) | `.../graphics/login-logo-alternative` |
| `logo_light.svg` | Login screen (light mode) | `.../graphics/login-logo` |
| `logo_blue.svg` | Alternative branding | N/A |
### Block Grid Demo Views (wwwroot/App_Plugins/Umbraco.BlockGridEditor.DefaultCustomViews/)
Pre-built AngularJS templates for block grid editor demos:
- `umbBlockGridDemoHeadlineBlock.html`
- `umbBlockGridDemoImageBlock.html`
- `umbBlockGridDemoRichTextBlock.html`
- `umbBlockGridDemoTwoColumnLayoutBlock.html`
---
## 6. Project-Specific Notes
### Static Web Asset Base Path
```xml
<StaticWebAssetBasePath>/</StaticWebAssetBasePath>
```
Assets are served from root path, not under assembly name.
### Compression Disabled
```xml
<CompressionEnabled>false</CompressionEnabled>
```
Comment notes `MapStaticAssets()` is not used (yet).
### Known Technical Debt
1. **Warning Suppression** (`.csproj:14-16`): `NU5123` - File paths too long for NuGet package. TODO indicates files should be renamed.
2. **Excluded Content** (`.csproj:31`): `wwwroot/umbraco/assets/README.md` explicitly excluded from package.
### Backoffice Localization
The backoffice includes language files for 25+ languages in `wwwroot/umbraco/backoffice/assets/lang/`:
- ar, bs, cs, cy, da, de, en, en-us, es, fr, he, hr, it, ja, ko, nb, nl, pl, pt, pt-br, ro, ru, sv, tr, uk, zh, zh-tw
### Monaco Editor
Full Monaco code editor included at `wwwroot/umbraco/backoffice/monaco-editor/` for rich code editing in backoffice.
### Theming
CSS themes in `wwwroot/umbraco/backoffice/css/`:
- `umb-css.css` - Main styles
- `uui-css.css` - UI library styles
- `dark.theme.css` - Dark theme
- `high-contrast.theme.css` - Accessibility theme
- `umbraco-blockgridlayout.css` - Block grid styles
- `rte-content.css` - Rich text editor content styles
---
## Quick Reference
### Essential Files
| File | Purpose |
|------|---------|
| `umbraco/UmbracoBackOffice/Index.cshtml` | Backoffice entry point |
| `umbraco/UmbracoLogin/Index.cshtml` | Login page |
| `wwwroot/umbraco/assets/README.md` | Asset documentation |
| `Umbraco.Cms.StaticAssets.csproj` | Build targets for frontend |
### Related Projects
| Project | Relationship |
|---------|--------------|
| `Umbraco.Web.UI.Client` | Source for backoffice assets (npm build) |
| `Umbraco.Web.UI.Login` | Source for login assets (npm build) |
| `Umbraco.Cms.Api.Management` | Razor view dependencies |
| `Umbraco.Web.Website` | Razor view dependencies |
### Build Commands (Manual)
```bash
# Build backoffice assets (from Umbraco.Web.UI.Client)
cd src/Umbraco.Web.UI.Client
npm install
npm run build:for:cms
# Build login assets (from Umbraco.Web.UI.Login)
cd src/Umbraco.Web.UI.Login
npm install
npm run build
```
### Preserve Assets During Clean
Create marker file to prevent asset deletion:
```bash
touch preserve.backoffice # In solution root
touch preserve.login # In solution root
```

View File

@@ -0,0 +1,293 @@
# Umbraco.Examine.Lucene
Full-text search provider for Umbraco CMS using Examine and Lucene.NET. Provides content, media, member, and Delivery API indexing with configurable directory factories and index diagnostics.
**Project Type**: Class Library (NuGet package)
**Target Framework**: net10.0
**Package ID**: Umbraco.Cms.Examine.Lucene
**Namespace**: Umbraco.Cms.Infrastructure.Examine
**Dependencies**: Umbraco.Infrastructure, Examine (Lucene.NET wrapper)
---
## 1. Architecture
### Project Purpose
This project provides Lucene-based full-text search capabilities for Umbraco:
1. **Index Types** - Content, Media, Member, and Delivery API indexes
2. **Directory Factories** - Configurable index storage (filesystem, temp, synced)
3. **Index Diagnostics** - Health checks and metadata for backoffice
4. **Backoffice Search** - Unified search across content tree with permissions
### Folder Structure
```
Umbraco.Examine.Lucene/
├── DependencyInjection/
│ ├── ConfigureIndexOptions.cs # Per-index configuration (77 lines)
│ └── UmbracoBuilderExtensions.cs # AddExamineIndexes() registration (64 lines)
├── Extensions/
│ └── ExamineExtensions.cs # Lucene query parsing, health check
├── AddExamineComposer.cs # Auto-registration via IComposer
├── BackOfficeExamineSearcher.cs # Unified backoffice search (532 lines)
├── ConfigurationEnabledDirectoryFactory.cs # Directory factory selector
├── DeliveryApiContentIndex.cs # Headless API index with culture support
├── LuceneIndexDiagnostics.cs # Base diagnostics implementation
├── LuceneIndexDiagnosticsFactory.cs # Diagnostics factory
├── LuceneRAMDirectoryFactory.cs # In-memory directory for testing
├── NoPrefixSimpleFsLockFactory.cs # Lock factory without prefix
├── UmbracoApplicationRoot.cs # Application root path provider
├── UmbracoContentIndex.cs # Content/media index (158 lines)
├── UmbracoExamineIndex.cs # Base index class (153 lines)
├── UmbracoExamineIndexDiagnostics.cs # Extended diagnostics
├── UmbracoLockFactory.cs # Lock factory wrapper
├── UmbracoMemberIndex.cs # Member index
└── UmbracoTempEnvFileSystemDirectoryFactory.cs # Temp directory factory
```
### Index Hierarchy
```
LuceneIndex (Examine)
└── UmbracoExamineIndex (base for all Umbraco indexes)
├── UmbracoContentIndex (content + media)
├── UmbracoMemberIndex (members)
└── DeliveryApiContentIndex (headless API)
```
---
## 2. Commands
**For Git workflow and build commands**, see [repository root](../../CLAUDE.md).
---
## 3. Key Components
### Built-in Indexes (Constants.UmbracoIndexes)
| Index Name | Type | Purpose |
|------------|------|---------|
| `InternalIndex` | UmbracoContentIndex | Backoffice search (all content) |
| `ExternalIndex` | UmbracoContentIndex | Frontend published content |
| `MembersIndex` | UmbracoMemberIndex | Member search |
| `DeliveryApiContentIndex` | DeliveryApiContentIndex | Headless API content |
**Note:** See [ConfigureIndexOptions](#configureindexoptions-dependencyinjectionconfigureindexoptionscs) for per-index analyzer and validator configuration.
### UmbracoExamineIndex (UmbracoExamineIndex.cs)
Base class for all Umbraco indexes. Key features:
**Runtime State Check** (lines 84-95):
```csharp
protected bool CanInitialize()
{
var canInit = _runtimeState.Level == RuntimeLevel.Run;
// Logs warning once if runtime not ready
}
```
Prevents indexing during install/upgrade.
**Special Field Transformations** (lines 130-152):
- `__Path` field - Enables descendant queries via path matching
- `__Icon` field - Preserves icon for display
**Raw Field Storage** (lines 101-118):
Fields prefixed with `__Raw_` are stored as `StoredField` (not analyzed), used for returning exact values.
### UmbracoContentIndex (UmbracoContentIndex.cs)
Handles content and media indexing with validation and cascade deletes.
**Cascade Delete** (lines 128-157):
When content deleted, automatically finds and removes all descendants:
```csharp
var descendantPath = $@"\-1\,*{nodeId}\,*";
var rawQuery = $"{UmbracoExamineFieldNames.IndexPathFieldName}:{descendantPath}";
```
**Validation Groups** (lines 66-116):
- `Valid` - Index normally
- `Failed` - Skip (invalid data)
- `Filtered` - Delete from index (moved to recycle bin)
### DeliveryApiContentIndex (DeliveryApiContentIndex.cs)
Specialized index for Delivery API with culture support.
**Key Differences** (lines 20-34):
- `ApplySpecialValueTransformations = false` - No path/icon transformations
- `PublishedValuesOnly = false` - Handles via populator
- `EnableDefaultEventHandler = false` - Custom event handling
**Composite IDs** (lines 118-128):
Index IDs can be composite: `"1234|da-DK"` (content ID + culture) or simple: `"1234"`.
### BackOfficeExamineSearcher (BackOfficeExamineSearcher.cs)
Unified search for backoffice tree with user permissions.
**Search Features**:
- Node name boosting (10x for exact match, line 316-327)
- Wildcard support for partial matches (line 350-376)
- User start node filtering (lines 378-456)
- Recycle bin filtering (lines 185-191)
- Multi-language variant search (all `nodeName_{lang}` fields)
**Entity Type Routing** (lines 89-155): Member→MembersIndex, Media→InternalIndex, Document→InternalIndex
---
## 4. Directory Factories
### Configuration Options (IndexCreatorSettings.LuceneDirectoryFactory)
- `Default` - FileSystemDirectoryFactory (standard filesystem storage)
- `TempFileSystemDirectoryFactory` - UmbracoTempEnvFileSystemDirectoryFactory (store in `%TEMP%/ExamineIndexes`)
- `SyncedTempFileSystemDirectoryFactory` - SyncedFileSystemDirectoryFactory (temp with sync to persistent)
### UmbracoTempEnvFileSystemDirectoryFactory (lines 20-34)
Creates unique temp path using site name + application identifier hash:
```csharp
var hashString = hostingEnvironment.SiteName + "::" + applicationIdentifier.GetApplicationUniqueIdentifier();
var cachePath = Path.Combine(Path.GetTempPath(), "ExamineIndexes", appDomainHash);
```
**Purpose**: Prevents index collisions when same app moves between workers in load-balanced scenarios.
### ConfigurationEnabledDirectoryFactory (lines 34-62)
Selector that creates appropriate factory based on `IndexCreatorSettings.LuceneDirectoryFactory` config value.
---
## 5. Index Configuration
### ConfigureIndexOptions (DependencyInjection/ConfigureIndexOptions.cs)
Configures per-index options via `IConfigureNamedOptions<LuceneDirectoryIndexOptions>`.
**Per-Index Settings** (lines 39-61):
| Index | Analyzer | Validator |
|-------|----------|-----------|
| InternalIndex | CultureInvariantWhitespaceAnalyzer | ContentValueSetValidator |
| ExternalIndex | StandardAnalyzer | PublishedContentValueSetValidator |
| MembersIndex | CultureInvariantWhitespaceAnalyzer | MemberValueSetValidator |
| DeliveryApiContentIndex | StandardAnalyzer | None (populator handles) |
**Global Settings** (lines 63-70):
- `UnlockIndex = true` - Always unlock on startup
- Snapshot deletion policy when using SyncedTempFileSystemDirectoryFactory
---
## 6. Index Diagnostics
### LuceneIndexDiagnostics (LuceneIndexDiagnostics.cs)
Provides health checks and metadata for backoffice examine dashboard.
**Health Check** (lines 41-45):
```csharp
public Attempt<string?> IsHealthy()
{
var isHealthy = Index.IsHealthy(out Exception? indexError);
return isHealthy ? Attempt<string?>.Succeed() : Attempt.Fail(indexError?.Message);
}
```
**Metadata** (lines 51-92):
- CommitCount, DefaultAnalyzer, LuceneDirectory type
- LuceneIndexFolder (relative path)
- DirectoryFactory and IndexDeletionPolicy types
### UmbracoExamineIndexDiagnostics (UmbracoExamineIndexDiagnostics.cs)
Extends base with Umbraco-specific metadata (lines 23-48):
- EnableDefaultEventHandler
- PublishedValuesOnly
- Validator settings (IncludeItemTypes, ExcludeItemTypes, etc.)
---
## 7. Project-Specific Notes
### Auto-Registration
`AddExamineComposer` (lines 10-14) automatically registers all Examine services:
```csharp
builder
.AddExamine()
.AddExamineIndexes();
```
### Service Registrations (UmbracoBuilderExtensions.cs)
Key services registered (lines 18-62): `IBackOfficeExamineSearcher`, `IIndexDiagnosticsFactory`, `IApplicationRoot`, `ILockFactory`, plus all 4 indexes with `ConfigurationEnabledDirectoryFactory`.
### Known Technical Debt
1. **Warning Suppression** (`.csproj:10-14`): `CS0618` - Uses obsolete members in Examine/Lucene.NET
2. **TODO: Raw Query Support** (`BackOfficeExamineSearcher.cs:71-76`):
```csharp
// TODO: WE should try to allow passing in a lucene raw query, however we will still need to do some manual string
// manipulation for things like start paths, member types, etc...
```
3. **TODO: Query Parsing** (`ExamineExtensions.cs:21-22`):
```csharp
// TODO: I'd assume there would be a more strict way to parse the query but not that i can find yet
```
### Index Field Names
Key fields from `UmbracoExamineFieldNames` (defined in Umbraco.Core):
- `__Path` - Content path for descendant queries
- `__Icon` - Content icon
- `__Raw_*` - Raw stored values (not analyzed)
- `__Key` - Content GUID
- `__NodeTypeAlias` - Content type alias
- `__IndexType` - `content`, `media`, or `member`
### Lucene Query Escaping
`BackOfficeExamineSearcher.BuildQuery()` (lines 193-311) handles:
- Special characters `*`, `-`, `_` removal/replacement
- Quoted phrase search (`"exact match"`)
- Query escaping via `QueryParserBase.Escape()`
- Path escaping (replace `-` with `\-`, `,` with `\,`)
### User Start Node Filtering
`BackOfficeExamineSearcher.AppendPath()` (lines 378-456) ensures users only see content they have access to:
1. Gets user's start nodes from `IBackOfficeSecurityAccessor`
2. Filters search results to paths under start nodes
3. Returns empty results if searchFrom outside user's access
---
## Quick Reference
**Index Names** (Constants.UmbracoIndexes):
- `InternalIndexName` = "InternalIndex"
- `ExternalIndexName` = "ExternalIndex"
- `MembersIndexName` = "MembersIndex"
- `DeliveryApiContentIndexName` = "DeliveryApiContentIndex"
**Key Interfaces**:
- `IUmbracoIndex` - Marker for Umbraco indexes
- `IUmbracoContentIndex` - Content index marker
- `IBackOfficeExamineSearcher` - Backoffice search service
- `IIndexDiagnostics` - Health/metadata for dashboard
**Dependencies**:
- `Umbraco.Infrastructure` - Core services, index populators
- `Umbraco.Core` - Field names, constants, interfaces
- `Examine` (NuGet) - Lucene.NET abstraction

View File

@@ -0,0 +1,362 @@
# Umbraco.PublishedCache.HybridCache
Published content caching layer for Umbraco CMS using Microsoft's HybridCache (in-memory + optional distributed cache). Provides high-performance content delivery with cache seeding, serialization, and notification-based invalidation.
**Project Type**: Class Library (NuGet package)
**Target Framework**: net10.0
**Package ID**: Umbraco.Cms.PublishedCache.HybridCache
**Namespace**: Umbraco.Cms.Infrastructure.HybridCache
**Dependencies**: Umbraco.Core, Umbraco.Infrastructure, Microsoft.Extensions.Caching.Hybrid, MessagePack, K4os.Compression.LZ4
---
## 1. Architecture
### Project Purpose
This project implements the published content cache using Microsoft's HybridCache pattern:
1. **Multi-Level Caching** - L1 (in-memory) + L2 (optional distributed cache like Redis)
2. **Cache Seeding** - Pre-populates cache on startup with frequently accessed content
3. **MessagePack Serialization** - Fast binary serialization with LZ4 compression
4. **Notification-Based Invalidation** - Automatic cache updates on content changes
5. **Draft/Published Separation** - Separate cache entries for draft and published content
### Folder Structure
```
Umbraco.PublishedCache.HybridCache/
├── DependencyInjection/
│ └── UmbracoBuilderExtensions.cs # AddUmbracoHybridCache() registration (90 lines)
├── Extensions/
│ └── HybridCacheExtensions.cs # ExistsAsync extension method
├── Factories/
│ ├── CacheNodeFactory.cs # Creates ContentCacheNode from IContent
│ ├── ICacheNodeFactory.cs
│ ├── IPublishedContentFactory.cs
│ └── PublishedContentFactory.cs # Creates IPublishedContent from cache
├── NotificationHandlers/
│ ├── CacheRefreshingNotificationHandler.cs # Content/media change handler (120 lines)
│ ├── HybridCacheStartupNotificationHandler.cs
│ └── SeedingNotificationHandler.cs # Startup seeding (39 lines)
├── Persistence/
│ ├── ContentSourceDto.cs # DTO for database queries
│ ├── DatabaseCacheRepository.cs # NPoco database access (891 lines)
│ └── IDatabaseCacheRepository.cs
├── SeedKeyProviders/
│ ├── BreadthFirstKeyProvider.cs # Base breadth-first traversal
│ ├── Document/
│ │ ├── ContentTypeSeedKeyProvider.cs # Seeds by content type
│ │ └── DocumentBreadthFirstKeyProvider.cs # Seeds top-level content (83 lines)
│ └── Media/
│ └── MediaBreadthFirstKeyProvider.cs
├── Serialization/
│ ├── ContentCacheDataModel.cs # Serializable cache model
│ ├── HybridCacheSerializer.cs # MessagePack serializer (40 lines)
│ ├── IContentCacheDataSerializer.cs # Nested data serializer interface
│ ├── JsonContentNestedDataSerializer.cs # JSON serializer option
│ ├── MsgPackContentNestedDataSerializer.cs # MessagePack serializer option
│ └── LazyCompressedString.cs # Lazy LZ4 compression
├── Services/
│ ├── DocumentCacheService.cs # Content caching service (372 lines)
│ ├── MediaCacheService.cs # Media caching service
│ ├── MemberCacheService.cs # Member caching service
│ └── DomainCacheService.cs # Domain caching service
├── CacheManager.cs # ICacheManager facade (27 lines)
├── ContentCacheNode.cs # Cache entry model (24 lines)
├── ContentData.cs # Content data container
├── ContentNode.cs # Content node representation
├── DatabaseCacheRebuilder.cs # Full cache rebuild
├── DocumentCache.cs # IPublishedContentCache impl (58 lines)
├── MediaCache.cs # IPublishedMediaCache impl
├── MemberCache.cs # IPublishedMemberCache impl
├── DomainCache.cs # IDomainCache impl
└── PublishedContent.cs # IPublishedContent impl
```
### Cache Architecture
```
Request → DocumentCache (IPublishedContentCache)
DocumentCacheService
┌─────────────────────────┐
│ HybridCache │
│ ┌───────┐ ┌────────┐ │
│ │ L1 │ │ L2 │ │
│ │Memory │→ │ Redis │ │
│ └───────┘ └────────┘ │
└─────────────────────────┘
↓ (cache miss)
DatabaseCacheRepository
cmsContentNu table
```
---
## 2. Commands
**For Git workflow and build commands**, see [repository root](../../CLAUDE.md).
---
## 3. Key Components
### HybridCache Configuration (UmbracoBuilderExtensions.cs)
**Registration** (lines 30-36):
```csharp
builder.Services.AddHybridCache(options =>
{
// Default 100MB max payload (configurable)
options.MaximumPayloadBytes = 1024 * 1024 * 100;
}).AddSerializer<ContentCacheNode, HybridCacheSerializer>();
```
**Key Services Registered** (lines 38-52):
- `IPublishedContentCache``DocumentCache`
- `IPublishedMediaCache``MediaCache`
- `IPublishedMemberCache``MemberCache`
- `IDomainCache``DomainCache`
- `IDocumentCacheService``DocumentCacheService`
- `IMediaCacheService``MediaCacheService`
- `ICacheManager``CacheManager`
### DocumentCacheService (Services/DocumentCacheService.cs)
Core caching logic for content.
**GetNodeAsync** (lines 110-155):
1. Check in-memory `_publishedContentCache` dictionary
2. Call `HybridCache.GetOrCreateAsync()` with database fallback
3. Verify published ancestor path exists
4. Convert `ContentCacheNode` to `IPublishedContent`
**Cache Key Format** (line 326):
```csharp
private static string GetCacheKey(Guid key, bool preview)
=> preview ? $"{key}+draft" : $"{key}";
```
**Cache Entry Options** (lines 275-287):
- Seed entries use `SeedCacheDuration` from `CacheSettings`
- Regular entries use `LocalCacheDuration` and `RemoteCacheDuration`
- Preview/draft entries always use regular (non-seed) durations
### ContentCacheNode (ContentCacheNode.cs)
Immutable cache entry model (marked `[ImmutableObject(true)]` for HybridCache performance):
- `Id`, `Key`, `SortOrder`, `CreateDate`, `CreatorId`
- `ContentTypeId`, `IsDraft`
- `ContentData? Data` - Property values, culture data, URL segments
### HybridCacheSerializer (Serialization/HybridCacheSerializer.cs)
MessagePack serialization with LZ4 compression:
```csharp
_options = defaultOptions
.WithCompression(MessagePackCompression.Lz4BlockArray)
.WithSecurity(MessagePackSecurity.UntrustedData);
```
---
## 4. Cache Seeding
### Seed Key Providers
| Provider | Purpose | Config Setting |
|----------|---------|----------------|
| `ContentTypeSeedKeyProvider` | Seeds specific content types | N/A |
| `DocumentBreadthFirstKeyProvider` | Seeds top-level content first | `DocumentBreadthFirstSeedCount` |
| `MediaBreadthFirstKeyProvider` | Seeds top-level media first | `MediaBreadthFirstSeedCount` |
### DocumentBreadthFirstKeyProvider (lines 28-82)
Breadth-first traversal that:
1. Gets root content keys
2. Filters to only published content (any culture)
3. Traverses children breadth-first until seed count reached
4. Only includes content with published ancestor path
### SeedingNotificationHandler (lines 27-38)
Runs on `UmbracoApplicationStartingNotification`:
- Skips during Install or Upgrade (when maintenance page shown)
- Calls `DocumentCacheService.SeedAsync()` then `MediaCacheService.SeedAsync()`
### Seed Process (DocumentCacheService.SeedAsync lines 205-264)
1. Collect seed keys from all `IDocumentSeedKeyProvider` instances
2. Process in batches of `DocumentSeedBatchSize`
3. Check if key already in cache via `ExistsAsync()`
4. Batch fetch uncached keys from database
5. Set in cache with seed entry options
---
## 5. Cache Invalidation
### CacheRefreshingNotificationHandler (120 lines)
Handles 8 notification types:
| Notification | Action |
|--------------|--------|
| `ContentRefreshNotification` | `RefreshContentAsync()` |
| `ContentDeletedNotification` | `DeleteItemAsync()` for each |
| `MediaRefreshNotification` | `RefreshMediaAsync()` |
| `MediaDeletedNotification` | `DeleteItemAsync()` for each |
| `ContentTypeRefreshedNotification` | Clear type cache, `Rebuild()` |
| `ContentTypeDeletedNotification` | Clear type cache |
| `MediaTypeRefreshedNotification` | Clear type cache, `Rebuild()` |
| `MediaTypeDeletedNotification` | Clear type cache |
### RefreshContentAsync (DocumentCacheService.cs lines 300-324)
1. Creates draft cache node from `IContent`
2. Persists to `cmsContentNu` table
3. If publishing: creates published node and persists
4. If unpublishing: clears published cache entry
### Cache Tags (line 331)
All content uses tag `Constants.Cache.Tags.Content` for bulk invalidation:
```csharp
await _hybridCache.RemoveByTagAsync(Constants.Cache.Tags.Content, cancellationToken);
```
---
## 6. Database Persistence
### DatabaseCacheRepository (891 lines)
NPoco-based repository for `cmsContentNu` table.
**Key Methods**:
- `GetContentSourceAsync(Guid key, bool preview)` - Single content fetch
- `GetContentSourcesAsync(IEnumerable<Guid> keys)` - Batch fetch
- `RefreshContentAsync(ContentCacheNode, PublishedState)` - Insert/update cache
- `Rebuild()` - Full cache rebuild by content type
**SQL Templates** (lines 575-748):
Uses `SqlContext.Templates` for cached SQL generation with optimized joins across:
- `umbracoNode`, `umbracoContent`, `cmsDocument`, `umbracoContentVersion`
- `cmsDocumentVersion`, `cmsContentNu`
### Serialization Options (NuCacheSettings.NuCacheSerializerType)
| Type | Serializer |
|------|------------|
| `JSON` | JsonContentNestedDataSerializer |
| `MessagePack` | MsgPackContentNestedDataSerializer |
---
## 7. Project-Specific Notes
### Experimental API Warning
HybridCache API is experimental (suppressed with `#pragma warning disable EXTEXP0018`).
### Draft vs Published Caching
- **Draft cache key**: `"{key}+draft"`
- **Published cache key**: `"{key}"`
- Each stored separately to avoid cross-contamination
- Draft entries never use seed durations
### Published Ancestor Path Check (DocumentCacheService.cs lines 131-135)
Before returning cached content, verifies ancestor path is published via `_publishStatusQueryService.HasPublishedAncestorPath()`. Returns null if parent unpublished.
### In-Memory Content Cache (DocumentCacheService.cs line 39)
Secondary `ConcurrentDictionary<string, IPublishedContent>` caches converted objects, since `ContentCacheNode` to `IPublishedContent` conversion is expensive.
### Known Technical Debt
1. **Warnings Disabled** (`.csproj:10-12`): `TreatWarningsAsErrors=false`
2. **Property Value Null Handling** (`PropertyData.cs:25, 37`): Cannot be null, needs fallback decision
3. **Routing Cache TODO** (`ContentCacheDataModel.cs:26`): Remove when routing cache implemented
4. **SQL Template Limitations** (`DatabaseCacheRepository.cs:612, 688, 737`): Can't use templates with certain joins
5. **Serializer Refactor** (`DatabaseCacheRepository.cs:481`): Standardize to ContentTypeId only
6. **String Interning** (`MsgPackContentNestedDataSerializer.cs:29`): Intern alias strings during deserialization
7. **V18 Interface Cleanup** (`IDatabaseCacheRepository.cs:24, 48`): Remove default implementations
### Cache Settings (CacheSettings)
Key configuration options:
- `DocumentBreadthFirstSeedCount` - Number of documents to seed
- `MediaBreadthFirstSeedCount` - Number of media items to seed
- `DocumentSeedBatchSize` - Batch size for seeding
- `Entry.Document.SeedCacheDuration` - Duration for seeded entries
- `Entry.Document.LocalCacheDuration` - L1 cache duration
- `Entry.Document.RemoteCacheDuration` - L2 cache duration
### InternalsVisibleTo
Test projects with access to internal types:
- `Umbraco.Tests`
- `Umbraco.Tests.Common`
- `Umbraco.Tests.UnitTests`
- `Umbraco.Tests.Integration`
- `DynamicProxyGenAssembly2` (for mocking)
---
## Quick Reference
### Cache Flow
```
GetByIdAsync(id) → GetByKeyAsync(key) → GetNodeAsync(key, preview)
→ HybridCache.GetOrCreateAsync()
→ DatabaseCacheRepository.GetContentSourceAsync() [on miss]
→ PublishedContentFactory.ToIPublishedContent()
→ CreateModel(IPublishedModelFactory)
```
### Key Interfaces
| Interface | Implementation | Purpose |
|-----------|----------------|---------|
| `IPublishedContentCache` | DocumentCache | Content retrieval |
| `IPublishedMediaCache` | MediaCache | Media retrieval |
| `IDocumentCacheService` | DocumentCacheService | Cache operations |
| `IDatabaseCacheRepository` | DatabaseCacheRepository | Database access |
| `ICacheNodeFactory` | CacheNodeFactory | Cache node creation |
### Configuration Keys
```json
{
"Umbraco": {
"CMS": {
"Cache": {
"DocumentBreadthFirstSeedCount": 100,
"MediaBreadthFirstSeedCount": 50,
"DocumentSeedBatchSize": 100,
"Entry": {
"Document": {
"SeedCacheDuration": "04:00:00",
"LocalCacheDuration": "00:05:00",
"RemoteCacheDuration": "01:00:00"
}
}
}
}
}
}
```
### Related Projects
| Project | Relationship |
|---------|--------------|
| `Umbraco.Core` | Interfaces (IPublishedContentCache, etc.) |
| `Umbraco.Infrastructure` | Services, repositories, NPoco |
| Microsoft.Extensions.Caching.Hybrid | HybridCache implementation |

View File

@@ -0,0 +1,380 @@
# Umbraco.Web.Common
Shared ASP.NET Core web functionality for Umbraco CMS. Provides controllers, middleware, application builder extensions, security/identity, localization, and the UmbracoContext request pipeline.
**Project Type**: Class Library (NuGet package)
**Target Framework**: net10.0
**Package ID**: Umbraco.Cms.Web.Common
**Namespace**: Umbraco.Cms.Web.Common
**Dependencies**: Umbraco.Examine.Lucene, Umbraco.PublishedCache.HybridCache, MiniProfiler, Serilog, Asp.Versioning
---
## 1. Architecture
### Project Purpose
This project provides the web layer foundation for Umbraco CMS:
1. **DI Registration** - `AddUmbraco()` entry point for service registration
2. **Application Pipeline** - `UmbracoApplicationBuilder` for middleware ordering
3. **Controllers** - Base classes for frontend and backoffice controllers
4. **UmbracoContext** - Request-scoped context with published content access
5. **Security** - Member sign-in, identity management, authentication middleware
6. **Middleware** - Boot failure handling, preview authentication, request routing
### Folder Structure
```
Umbraco.Web.Common/
├── ApplicationBuilder/
│ ├── IUmbracoApplicationBuilder.cs # Builder interface
│ ├── IUmbracoPipelineFilter.cs # Pipeline customization hooks
│ └── UmbracoApplicationBuilder.cs # Middleware orchestration (170 lines)
├── AspNetCore/
│ ├── AspNetCoreIpResolver.cs # IP address resolution
│ ├── AspNetCorePasswordHasher.cs # Identity password hashing
│ ├── AspNetCoreRequestAccessor.cs # Request access abstraction
│ └── OptionsMonitorAdapter.cs # Config monitoring wrapper
├── Controllers/
│ ├── IRenderController.cs # Frontend controller marker
│ ├── IVirtualPageController.cs # Virtual page support
│ ├── PluginController.cs # Plugin controller base (104 lines)
│ ├── UmbracoApiController.cs # Legacy API controller (obsolete)
│ ├── UmbracoAuthorizedController.cs # Backoffice authorized base
│ └── UmbracoController.cs # Base MVC controller (13 lines)
├── DependencyInjection/
│ └── UmbracoBuilderExtensions.cs # AddUmbraco(), AddUmbracoCore() (338 lines)
├── Extensions/
│ ├── BlockGridTemplateExtensions.cs # Block grid Razor helpers
│ ├── BlockListTemplateExtensions.cs # Block list Razor helpers
│ ├── HttpRequestExtensions.cs # Request utility methods
│ ├── ImageCropperTemplateExtensions.cs # Image crop Razor helpers
│ ├── LinkGeneratorExtensions.cs # URL generation helpers
│ ├── WebApplicationExtensions.cs # BootUmbracoAsync() (32 lines)
│ └── [30+ extension classes]
├── Filters/
│ ├── BackOfficeCultureFilter.cs # Culture detection
│ ├── DisableBrowserCacheAttribute.cs # Cache control headers
│ ├── UmbracoMemberAuthorizeAttribute.cs # Member authorization
│ └── ValidateUmbracoFormRouteStringAttribute.cs
├── Localization/
│ ├── UmbracoBackOfficeIdentityCultureProvider.cs
│ └── UmbracoPublishedContentCultureProvider.cs
├── Middleware/
│ ├── BootFailedMiddleware.cs # Startup failure handling (81 lines)
│ └── PreviewAuthenticationMiddleware.cs # Preview mode auth (84 lines)
├── Routing/
│ ├── IAreaRoutes.cs # Area routing interface
│ ├── IRoutableDocumentFilter.cs # Content routing filter
│ ├── UmbracoRouteValues.cs # Route data container (60 lines)
│ └── UmbracoVirtualPageRoute.cs # Virtual page routing
├── Security/
│ ├── MemberSignInManager.cs # Member sign-in (350 lines)
│ ├── UmbracoSignInManager.cs # Base sign-in manager
│ ├── IMemberSignInManager.cs # Sign-in interface
│ └── EncryptionHelper.cs # Surface controller encryption
├── Templates/
│ └── TemplateRenderer.cs # Razor template rendering
├── UmbracoContext/
│ ├── UmbracoContext.cs # Request context (173 lines)
│ └── UmbracoContextFactory.cs # Context creation (75 lines)
├── UmbracoHelper.cs # Template helper (427 lines)
└── Umbraco.Web.Common.csproj # Project configuration (48 lines)
```
### Request Pipeline Flow
```
WebApplication.BootUmbracoAsync()
UseUmbraco().WithMiddleware()
┌──────────────────────────────┐
│ UmbracoApplicationBuilder │
│ ├─ RunPrePipeline() │
│ ├─ BootFailedMiddleware │
│ ├─ UseUmbracoCore() │
│ ├─ UseStaticFiles() │
│ ├─ UseRouting() │
│ ├─ UseAuthentication() │
│ ├─ UseAuthorization() │
│ ├─ UseRequestLocalization() │
│ └─ RunPostPipeline() │
└──────────────────────────────┘
WithEndpoints() → Content routing
```
---
## 2. Commands
**For Git workflow and build commands**, see [repository root](../../CLAUDE.md).
---
## 3. Key Components
### DI Entry Point (DependencyInjection/UmbracoBuilderExtensions.cs)
**AddUmbraco()** (lines 77-123) - Creates `IUmbracoBuilder` and registers core services:
```csharp
public static IUmbracoBuilder AddUmbraco(
this IServiceCollection services,
IWebHostEnvironment webHostEnvironment,
IConfiguration config)
```
Key registrations:
- Sets `DataDirectory` for database paths (line 97-99)
- Creates `HttpContextAccessor` singleton (line 104-105)
- Initializes `AppCaches` with request-scoped cache
- Creates `TypeLoader` for assembly scanning
- Returns `UmbracoBuilder` for fluent configuration
**AddUmbracoCore()** (lines 131-173) - Registers ASP.NET Core-specific services:
- `IHostingEnvironment``AspNetCoreHostingEnvironment`
- `IBackOfficeInfo``AspNetCoreBackOfficeInfo`
- `IDbProviderFactoryCreator` with SQL syntax providers
- Telemetry providers and application lifetime
**AddWebComponents()** (lines 222-288) - Registers web-specific services:
- Session with `UMB_SESSION` cookie (line 229)
- API versioning configuration
- Password hasher, cookie manager, IP resolver
- `UmbracoHelper`, `UmbracoContextFactory`
- All middleware as singletons
### Application Builder (ApplicationBuilder/UmbracoApplicationBuilder.cs)
Orchestrates middleware registration in correct order.
**WithMiddleware()** (lines 49-65):
1. `RunPrePipeline()` - Custom filters before Umbraco middleware
2. `RegisterDefaultRequiredMiddleware()` - Core middleware stack
3. `RunPostPipeline()` - Custom filters after
4. User-provided middleware callback
**RegisterDefaultRequiredMiddleware()** (lines 70-104):
```csharp
UseUmbracoCoreMiddleware();
AppBuilder.UseUmbracoMediaFileProvider();
AppBuilder.UseUmbracoBackOfficeRewrites();
AppBuilder.UseStaticFiles();
AppBuilder.UseUmbracoPluginsStaticFiles();
AppBuilder.UseRouting();
AppBuilder.UseAuthentication();
AppBuilder.UseAuthorization();
AppBuilder.UseAntiforgery();
AppBuilder.UseRequestLocalization();
AppBuilder.UseSession();
```
**Pipeline Filter Hooks** (IUmbracoPipelineFilter):
- `OnPrePipeline()` - Before any Umbraco middleware
- `OnPreRouting()` - Before UseRouting()
- `OnPostRouting()` - After UseRouting()
- `OnPostPipeline()` - After all Umbraco middleware
- `OnEndpoints()` - Before endpoint mapping
### UmbracoContext (UmbracoContext/UmbracoContext.cs)
Request-scoped container for Umbraco request state.
**Key Properties**:
- `Content``IPublishedContentCache` (line 102)
- `Media``IPublishedMediaCache` (line 105)
- `Domains``IDomainCache` (line 108)
- `PublishedRequest` → Routed content for request (line 111)
- `InPreviewMode` → Preview cookie detected (lines 140-152)
- `OriginalRequestUrl` / `CleanedUmbracoUrl` - Request URLs
**Preview Detection** (lines 154-166):
```csharp
private void DetectPreviewMode()
{
// Check preview cookie, verify not backoffice request
var previewToken = _cookieManager.GetCookieValue(Constants.Web.PreviewCookieName);
_previewing = _previewToken.IsNullOrWhiteSpace() == false;
}
```
### UmbracoHelper (UmbracoHelper.cs)
Template helper for Razor views (scoped lifetime).
**Content Retrieval** (lines 192-317):
- `Content(id)` - Get by int, Guid, string, or Udi
- `Content(ids)` - Batch retrieval
- `ContentAtRoot()` - Root content items
**Media Retrieval** (lines 320-424):
- Same pattern as content
**Dictionary** (lines 102-189):
- `GetDictionaryValue(key)` - Localized string lookup
- `GetDictionaryValueOrDefault(key, defaultValue)`
- `CultureDictionary` - Current culture dictionary
### Controller Base Classes
| Controller | Purpose | Features |
|------------|---------|----------|
| `UmbracoController` | Base MVC controller | Simple base, debug InstanceId |
| `UmbracoAuthorizedController` | Backoffice controllers | `[Authorize(BackOfficeAccess)]`, `[DisableBrowserCache]` |
| `PluginController` | Plugin/package controllers | UmbracoContext, Services, AppCaches, ProfilingLogger |
| `UmbracoApiController` | Legacy API controller | **Obsolete** - Use ASP.NET Core ApiController |
| `IRenderController` | Frontend rendering marker | Route hijacking support |
**PluginController** (lines 18-104):
- Provides `UmbracoContext`, `DatabaseFactory`, `Services`, `AppCaches`
- Static metadata caching with `ConcurrentDictionary<Type, PluginControllerMetadata>`
- Auto-discovers `[PluginController]` and `[IsBackOffice]` attributes
### Member Sign-In (Security/MemberSignInManager.cs)
ASP.NET Core Identity sign-in manager for members.
**Key Features**:
- External login with auto-linking (lines 112-142)
- Two-factor authentication support (lines 162-172)
- Auto-link and create member accounts (lines 180-267)
**Auto-Link Flow** (lines 180-267):
1. Check if auto-link enabled and email available
2. Find or create member by email
3. Call `OnAutoLinking` callback for customization
4. Add external login link
5. Sign in or request 2FA
**Result Types** (lines 320-348):
- `ExternalLoginSignInResult.NotAllowed` - Login refused by callback
- `AutoLinkSignInResult.FailedNoEmail` - No email from provider
- `AutoLinkSignInResult.FailedCreatingUser` - User creation failed
- `AutoLinkSignInResult.FailedLinkingUser` - Link creation failed
### Middleware
**BootFailedMiddleware** (lines 17-81):
- Intercepts requests when `RuntimeLevel == BootFailed`
- Debug mode: Rethrows exception for stack trace
- Production: Shows `BootFailed.html` error page
**PreviewAuthenticationMiddleware** (lines 22-84):
- Adds backoffice identity to principal for preview requests
- Skips client-side requests and backoffice paths
- Uses `IPreviewService.TryGetPreviewClaimsIdentityAsync()`
---
## 4. Routing
### UmbracoRouteValues (Routing/UmbracoRouteValues.cs)
Container for routed request data:
- `PublishedRequest` - The resolved content request
- `ControllerActionDescriptor` - MVC routing info
- `TemplateName` - Resolved template name
- `DefaultActionName` = "Index"
### Route Hijacking
Implement `IRenderController` to handle specific content types:
```csharp
public class ProductController : Controller, IRenderController
{
public IActionResult Index() => View();
}
```
### Virtual Pages
Implement `IVirtualPageController` for URL-to-content mapping without physical content nodes.
---
## 5. Project-Specific Notes
### Warning Suppressions (csproj lines 10-22)
Multiple analyzer warnings suppressed:
- SA1117, SA1401, SA1134 - StyleCop formatting
- ASP0019 - Header dictionary usage
- CS0618/SYSLIB0051 - Obsolete references
- IDE0040/SA1400 - Access modifiers
- SA1649 - File name matching
### InternalsVisibleTo
```xml
<InternalsVisibleTo>Umbraco.Tests.UnitTests</InternalsVisibleTo>
```
### Known Technical Debt
1. **MVC Global State** (UmbracoBuilderExtensions.cs:210-211): `AddControllersWithViews` modifies global app, order matters
2. **OptionsMonitor Hack** (AspNetCore/OptionsMonitorAdapter.cs:6): Temporary workaround for TypeLoader during ConfigureServices
3. **DisposeResources TODO** (UmbracoContext.cs:168-171): Empty dispose method marked for removal
4. **Pipeline Default Implementations** (IUmbracoPipelineFilter.cs:36,45): Default methods to remove in Umbraco 13
5. **SignIn Manager Sharing** (MemberSignInManager.cs:319,325): Could share code with backoffice sign-in
### Session Configuration
Default session cookie (lines 227-231):
```csharp
options.Cookie.Name = "UMB_SESSION";
options.Cookie.HttpOnly = true;
```
Can be overridden by calling `AddSession` after `AddWebComponents`.
---
## Quick Reference
### Startup Flow
```csharp
// Program.cs
builder.Services.AddUmbraco(webHostEnvironment, config)
.AddUmbracoCore()
.AddMvcAndRazor()
.AddWebComponents()
.AddUmbracoProfiler()
.Build();
await app.BootUmbracoAsync();
app.UseUmbraco()
.WithMiddleware(u => u.UseBackOffice())
.WithEndpoints(u => u.UseBackOfficeEndpoints());
```
### Key Interfaces
| Interface | Implementation | Purpose |
|-----------|----------------|---------|
| `IUmbracoContext` | UmbracoContext | Request state |
| `IUmbracoContextFactory` | UmbracoContextFactory | Context creation |
| `IUmbracoContextAccessor` | (in Core) | Context access |
| `IMemberSignInManager` | MemberSignInManager | Member auth |
| `IUmbracoApplicationBuilder` | UmbracoApplicationBuilder | Pipeline config |
### Extension Method Namespaces
Most extensions are in `Umbraco.Extensions` namespace:
- `HttpRequestExtensions` - Request helpers
- `LinkGeneratorExtensions` - URL generation
- `BlockGridTemplateExtensions` - Block grid rendering
- `ImageCropperTemplateExtensions` - Image cropping
### Related Projects
| Project | Relationship |
|---------|--------------|
| `Umbraco.Core` | Interface contracts |
| `Umbraco.Infrastructure` | Service implementations |
| `Umbraco.Examine.Lucene` | Search dependency |
| `Umbraco.PublishedCache.HybridCache` | Caching dependency |
| `Umbraco.Web.UI` | Main web application (references this) |
| `Umbraco.Cms.Api.Common` | API layer (references this) |

View File

@@ -0,0 +1,266 @@
# Umbraco.Web.UI.Login
TypeScript/Lit login SPA for Umbraco CMS backoffice authentication. Provides the `<umb-auth>` web component used in the login page, supporting local login, MFA, password reset, and user invitation flows.
**Project Type**: TypeScript Library (Vite)
**Runtime**: Node.js >= 22, npm >= 10.9
**Output**: ES Module library → `../Umbraco.Cms.StaticAssets/wwwroot/umbraco/login/`
**Dependencies**: @umbraco-cms/backoffice, Lit, Vite
---
## 1. Architecture
### Project Purpose
This project builds the login single-page application:
1. **Web Component** - `<umb-auth>` custom element for authentication
2. **Authentication Flows** - Login, MFA, password reset, user invitation
3. **Localization** - Multi-language support (en, de, da, nb, nl, sv)
4. **API Client** - Generated from OpenAPI specification
### Folder Structure
```
Umbraco.Web.UI.Login/
├── src/
│ ├── api/ # Generated API client
│ │ ├── client/ # HTTP client utilities
│ │ ├── core/ # Serializers, types
│ │ ├── sdk.gen.ts # Generated SDK
│ │ └── types.gen.ts # Generated types
│ ├── components/
│ │ ├── layouts/ # Layout components
│ │ │ ├── auth-layout.element.ts
│ │ │ ├── confirmation-layout.element.ts
│ │ │ ├── error-layout.element.ts
│ │ │ └── new-password-layout.element.ts
│ │ ├── pages/ # Page components
│ │ │ ├── login.page.element.ts
│ │ │ ├── mfa.page.element.ts
│ │ │ ├── new-password.page.element.ts
│ │ │ ├── reset-password.page.element.ts
│ │ │ └── invite.page.element.ts
│ │ └── back-to-login-button.element.ts
│ ├── contexts/
│ │ ├── auth.context.ts # Authentication state (86 lines)
│ │ └── auth.repository.ts # API calls (231 lines)
│ ├── controllers/
│ │ └── slim-backoffice-initializer.ts # Minimal backoffice bootstrap
│ ├── localization/
│ │ ├── lang/ # Language files (da, de, en, en-us, nb, nl, sv)
│ │ └── manifests.ts # Localization registration
│ ├── mocks/ # MSW mock handlers for development
│ │ ├── handlers/
│ │ │ ├── login.handlers.ts
│ │ │ └── backoffice.handlers.ts
│ │ └── data/login.data.ts
│ ├── utils/
│ │ ├── is-problem-details.function.ts
│ │ └── load-custom-view.function.ts
│ ├── auth.element.ts # Main <umb-auth> component (404 lines)
│ ├── types.ts # Type definitions
│ ├── manifests.ts # Extension manifests
│ └── umbraco-package.ts # Package definition
├── public/
│ └── favicon.svg
├── index.html # Development entry point
├── package.json # npm configuration (29 lines)
├── tsconfig.json # TypeScript config (25 lines)
├── vite.config.ts # Vite build config (20 lines)
├── .nvmrc # Node version (22)
└── .prettierrc.json # Prettier config
```
### Build Output
Built assets are output to `Umbraco.Cms.StaticAssets`:
```
../Umbraco.Cms.StaticAssets/wwwroot/umbraco/login/
├── login.js # Main ES module
└── login.js.map # Source map
```
---
## 2. Commands
**For Git workflow and build commands**, see [repository root](../../CLAUDE.md).
### Development
```bash
cd src/Umbraco.Web.UI.Login
# Install dependencies
npm install
# Run development server with hot reload
npm run dev
# Build for production (outputs to StaticAssets)
npm run build
# Watch mode build
npm run watch
# Preview production build
npm run preview
```
### API Generation
```bash
# Regenerate API client from OpenAPI spec
npm run generate:server-api
```
Uses `@hey-api/openapi-ts` to generate TypeScript client from the Management API OpenAPI specification.
---
## 3. Key Components
### UmbAuthElement (src/auth.element.ts)
Main `<umb-auth>` web component (404 lines).
**Attributes** (lines 172-204):
| Attribute | Type | Description |
|-----------|------|-------------|
| `disable-local-login` | boolean | Disables local login, external only |
| `background-image` | string | Login page background URL |
| `logo-image` | string | Logo URL |
| `logo-image-alternative` | string | Alternative logo (dark mode) |
| `username-is-email` | boolean | Use email as username |
| `allow-password-reset` | boolean | Show password reset link |
| `allow-user-invite` | boolean | Enable user invitation flow |
| `return-url` | string | Redirect URL after login |
**Authentication Flows** (lines 379-396):
- Default: Login page with username/password
- `flow=mfa`: Multi-factor authentication
- `flow=reset`: Request password reset
- `flow=reset-password`: Set new password
- `flow=invite-user`: User invitation acceptance
**Form Workaround** (lines 274-334):
Creates login form in light DOM (not shadow DOM) to support Chrome autofill/autocomplete, which doesn't work properly with shadow DOM inputs.
### UmbAuthContext (src/contexts/auth.context.ts)
Authentication state and API methods (86 lines):
- `login(data)` - Authenticate user
- `resetPassword(username)` - Request password reset
- `validatePasswordResetCode(userId, code)` - Validate reset code
- `newPassword(password, code, userId)` - Set new password
- `validateInviteCode(token, userId)` - Validate invitation
- `newInvitedUserPassword(...)` - Set invited user password
- `validateMfaCode(code, provider)` - MFA validation
**Return Path Security** (lines 37-54): Validates return URL to prevent open redirect attacks - only allows same-origin URLs.
### Localization
Supported languages: English (en, en-us), Danish (da), German (de), Norwegian Bokmål (nb), Dutch (nl), Swedish (sv).
---
## 4. Development with Mocks
### MSW (Mock Service Worker)
Development uses MSW for API mocking. Run `npm run dev` to start with mocks enabled.
**Mock Files**:
- `handlers/login.handlers.ts` - Login, MFA, password reset
- `handlers/backoffice.handlers.ts` - Backoffice API
- `data/login.data.ts` - Test user data
- `customViews/` - Example custom login views
---
## 5. Project-Specific Notes
### Shadow DOM Limitation
Chrome doesn't support autofill in shadow DOM inputs. The login form is created in light DOM via `#initializeForm()` (lines 274-334) and inserted with `insertAdjacentElement()`. See Chromium intent-to-ship discussion linked in code.
### Slim Backoffice Controller
`UmbSlimBackofficeController` provides minimal backoffice utilities (extension registry, localization, context API) without loading the full app.
### Localization Wait Pattern (lines 242-265)
Form waits for localization availability before rendering. Retries 40 times with 50ms interval (2 second max wait).
### External Dependencies
- `@umbraco-cms/backoffice` ^16.2.0 - Umbraco UI components, Lit element base
- `vite` ^7.2.0 - Build tooling
- `typescript` ^5.9.3 - Type checking
- `msw` ^2.11.3 - API mocking
- `@hey-api/openapi-ts` ^0.85.0 - API client generation
### Known Technical Debt
1. **UUI Color Variable** - Multiple files use `--uui-color-text-alt` with TODO to change when UUI adds muted text variable:
- `back-to-login-button.element.ts:35`
- `confirmation-layout.element.ts:41`
- `error-layout.element.ts:42`
- `login.page.element.ts:203,226`
- `new-password-layout.element.ts:221`
- `reset-password.page.element.ts:110`
2. **API Client Error Types** (`api/client/client.gen.ts:207`): Error handling and types need improvement
### TypeScript Configuration
Key settings in `tsconfig.json`:
- `target`: ES2022
- `experimentalDecorators`: true (for Lit decorators)
- `useDefineForClassFields`: false (Lit compatibility)
- `moduleResolution`: bundler (Vite)
- `strict`: true
---
## Quick Reference
### Essential Commands
```bash
# Development
npm run dev # Start dev server
npm run build # Build to StaticAssets
npm run watch # Watch mode
# API
npm run generate:server-api # Regenerate API client
```
### Key Files
| File | Purpose |
|------|---------|
| `src/auth.element.ts` | Main `<umb-auth>` component |
| `src/contexts/auth.context.ts` | Auth state management |
| `src/contexts/auth.repository.ts` | API calls |
| `vite.config.ts` | Build configuration |
| `package.json` | Dependencies and scripts |
### Build Output
Built files go to:
```
../Umbraco.Cms.StaticAssets/wwwroot/umbraco/login/
```
### Related Projects
| Project | Relationship |
|---------|--------------|
| `Umbraco.Cms.StaticAssets` | Receives built output |
| `Umbraco.Web.UI.Client` | Backoffice SPA (similar architecture) |
| `@umbraco-cms/backoffice` | NPM dependency for UI components |

View File

@@ -0,0 +1,320 @@
# Umbraco.Web.UI
Main ASP.NET Core web application for running Umbraco CMS. This is the development test site that references all Umbraco packages and provides a minimal startup configuration for testing and development.
**Project Type**: ASP.NET Core Web Application
**SDK**: Microsoft.NET.Sdk.Web
**Target Framework**: net10.0
**IsPackable**: false (not published as NuGet package)
**Namespace**: Umbraco.Cms.Web.UI
---
## 1. Architecture
### Project Purpose
This is the **runnable Umbraco instance** used for development and testing. It:
1. **References All Umbraco Packages** - Via the `Umbraco.Cms` meta-package
2. **Provides Minimal Startup** - Simple `Program.cs` with builder pattern
3. **Includes Development Tools** - Backoffice development mode support
4. **Demonstrates Patterns** - Example composers, views, and block templates
### Folder Structure
```
Umbraco.Web.UI/
├── Composers/
│ ├── ControllersAsServicesComposer.cs # DI validation composer (66 lines)
│ └── UmbracoAppAuthenticatorComposer.cs # 2FA example (commented out)
├── Properties/
│ └── launchSettings.json # Debug profiles (29 lines)
├── Views/
│ ├── _ViewImports.cshtml # Global view imports
│ ├── page.cshtml # Generic page template
│ ├── BlockPage.cshtml # Block page template
│ ├── BlockTester.cshtml # Block testing template
│ └── Partials/
│ ├── blockgrid/ # Block grid partials
│ │ ├── area.cshtml
│ │ ├── areas.cshtml
│ │ ├── items.cshtml
│ │ └── default.cshtml
│ ├── blocklist/ # Block list partials
│ │ ├── Components/textBlock.cshtml
│ │ └── default.cshtml
│ └── singleblock/ # Single block partials
│ └── default.cshtml
├── wwwroot/
│ └── favicon.ico # Site favicon
├── umbraco/ # Runtime data directory
│ ├── Data/ # SQLite database, temp files
│ │ ├── Umbraco.sqlite.db # Development database
│ │ └── TEMP/ # ModelsBuilder, DistCache
│ ├── Logs/ # Serilog JSON logs
│ └── models/ # Generated content models
├── appsettings.json # Production config
├── appsettings.Development.json # Development overrides
├── appsettings.template.json # Template for new installs
├── appsettings.Development.template.json # Dev template
├── appsettings-schema.json # JSON schema reference
├── appsettings-schema.Umbraco.Cms.json # Full Umbraco schema (71KB)
├── umbraco-package-schema.json # Package manifest schema (495KB)
├── Program.cs # Application entry point (33 lines)
└── Umbraco.Web.UI.csproj # Project file (75 lines)
```
### Project Dependencies
```xml
<ProjectReference Include="..\Umbraco.Cms\Umbraco.Cms.csproj" />
<ProjectReference Include="..\Umbraco.Cms.DevelopmentMode.Backoffice\..." />
```
- **Umbraco.Cms** - Meta-package referencing all Umbraco packages
- **Umbraco.Cms.DevelopmentMode.Backoffice** - Hot reload for backoffice development
---
## 2. Commands
**For Git workflow and build commands**, see [repository root](../../CLAUDE.md).
### Running the Application
```bash
# Run with Kestrel (recommended)
dotnet run --project src/Umbraco.Web.UI
# Run with watch for auto-reload
dotnet watch --project src/Umbraco.Web.UI
# Run from Visual Studio
# Profile: "Umbraco.Web.UI" (https://localhost:44339)
# Profile: "IIS Express" (http://localhost:11000)
```
### Database
SQLite is configured by default for development:
- Location: `src/Umbraco.Web.UI/umbraco/Data/Umbraco.sqlite.db`
- Connection string configured in `appsettings.json`
---
## 3. Key Components
### Program.cs (33 lines)
Minimal Umbraco startup using builder pattern:
```csharp
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.CreateUmbracoBuilder()
.AddBackOffice()
.AddWebsite()
#if UseDeliveryApi
.AddDeliveryApi()
#endif
.AddComposers()
.Build();
WebApplication app = builder.Build();
await app.BootUmbracoAsync();
app.UseUmbraco()
.WithMiddleware(u =>
{
u.UseBackOffice();
u.UseWebsite();
})
.WithEndpoints(u =>
{
u.UseBackOfficeEndpoints();
u.UseWebsiteEndpoints();
});
await app.RunAsync();
```
**Preprocessor Directives**:
- `#if UseDeliveryApi` - Enable/disable Delivery API
- `#if UseHttpsRedirect` - Enable/disable HTTPS redirection
### ControllersAsServicesComposer (lines 36-38)
Registers all controllers in the DI container for validation purposes:
```csharp
public void Compose(IUmbracoBuilder builder) => builder.Services
.AddMvc()
.AddControllersAsServicesWithoutChangingActivator();
```
**Purpose**: Detects ambiguous constructors in CMS controllers that would cause issues with `ServiceBasedControllerActivator`. Not shipped in `Umbraco.Templates`.
### Launch Profiles (Properties/launchSettings.json)
| Profile | URL | Command |
|---------|-----|---------|
| Umbraco.Web.UI | https://localhost:44339, http://localhost:11000 | Project |
| IIS Express | http://localhost:11000, SSL port 44339 | IISExpress |
---
## 4. Configuration
### appsettings.template.json
Default configuration for new installations:
| Setting | Value | Description |
|---------|-------|-------------|
| `ModelsMode` | `InMemoryAuto` | Auto-generate models in memory |
| `DefaultUILanguage` | `en-us` | Backoffice language |
| `HideTopLevelNodeFromPath` | `true` | Clean URLs |
| `TimeOut` | `00:20:00` | Session timeout |
| `UseHttps` | `false` | HTTPS enforcement |
| `UsernameIsEmail` | `true` | Email as username |
| `UserPassword.RequiredLength` | `10` | Minimum password length |
### appsettings.Development.template.json
Development-specific overrides:
| Setting | Value | Description |
|---------|-------|-------------|
| `Hosting.Debug` | `true` | Debug mode enabled |
| `LuceneDirectoryFactory` | `TempFileSystemDirectoryFactory` | Examine indexes in temp |
| Console logging | `Async` sink | Serilog console output |
| Examine log levels | `Debug` | Detailed Examine logging |
### Auto-Copy Build Target (csproj lines 65-73)
MSBuild targets automatically copy template files if missing (appsettings.json and appsettings.Development.json).
---
## 5. ModelsBuilder
### InMemoryAuto Mode
The project uses `InMemoryAuto` mode:
- Models auto-generated at runtime
- Source stored in `umbraco/Data/TEMP/InMemoryAuto/`
- Compiled assembly in `Compiled/` subdirectory
- `models.hash` tracks content type changes
### Generated Models Location
```
umbraco/models/*.generated.cs # Source files (for reference)
umbraco/Data/TEMP/InMemoryAuto/ # Runtime compilation
```
### Razor Compilation Settings (csproj lines 50-51)
Razor compilation is disabled for InMemoryAuto mode (`RazorCompileOnBuild=false`, `RazorCompileOnPublish=false`).
---
## 6. ICU Globalization
### App-Local ICU (csproj lines 39-40)
Uses app-local ICU (`Microsoft.ICU.ICU4C.Runtime` v72.1.0.3) for consistent globalization across platforms.
**Note**: Ensure ICU version matches between package reference and runtime option. Changes must also be made to `Umbraco.Templates`.
---
## 7. Project-Specific Notes
### Development vs Production
This project is for **development only**:
- `IsPackable=false` - Not published as NuGet
- `EnablePackageValidation=false` - No package validation
- References `DevelopmentMode.Backoffice` for hot reload
### Package Version Management
```xml
<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
```
Does NOT use central package management. Versions specified directly:
- `Microsoft.EntityFrameworkCore.Design` - For EF Core migrations tooling
- `Microsoft.Build.Tasks.Core` - Security fix for EFCore.Design dependency
- `Microsoft.ICU.ICU4C.Runtime` - Globalization
### Umbraco Targets Import (csproj lines 19-20)
Imports shared build configuration from `Umbraco.Cms.Targets` project (props and targets).
### Excluded Views (csproj lines 55-57)
Three Umbraco views excluded from content (UmbracoInstall, UmbracoLogin, UmbracoBackOffice) as they come from `Umbraco.Cms.StaticAssets` RCL.
### Known Technical Debt
1. **Warning Suppression** (csproj lines 12-16): `SA1119` - Unnecessary parenthesis to fix
### Runtime Data (umbraco/ directory)
| Directory | Contents | Git Status |
|-----------|----------|------------|
| `umbraco/Data/` | SQLite database, MainDom locks | Ignored |
| `umbraco/Logs/` | Serilog JSON trace logs | Ignored |
| `umbraco/models/` | Generated content models | Ignored |
| `umbraco/Data/TEMP/` | ModelsBuilder, DistCache | Ignored |
---
## Quick Reference
### Essential Commands
```bash
# Run development site
dotnet run --project src/Umbraco.Web.UI
# Clean runtime data (reset database)
rm -rf src/Umbraco.Web.UI/umbraco/Data
# Reset to fresh install
rm src/Umbraco.Web.UI/appsettings.json
rm src/Umbraco.Web.UI/appsettings.Development.json
```
### Important Files
| File | Purpose |
|------|---------|
| `Program.cs` | Application entry point |
| `appsettings.template.json` | Default configuration |
| `appsettings.Development.template.json` | Development overrides |
| `launchSettings.json` | Debug profiles |
| `Composers/ControllersAsServicesComposer.cs` | DI validation |
### URLs
| Environment | URL |
|-------------|-----|
| Development (Kestrel) | https://localhost:44339 |
| Development (HTTP) | http://localhost:11000 |
| Backoffice | /umbraco |
| Installer | /install (on first run) |
### Related Projects
| Project | Relationship |
|---------|--------------|
| `Umbraco.Cms` | Meta-package (all dependencies) |
| `Umbraco.Cms.DevelopmentMode.Backoffice` | Hot reload support |
| `Umbraco.Cms.StaticAssets` | Backoffice/login views |
| `Umbraco.Cms.Targets` | Build configuration |
| `Umbraco.Web.Common` | Web functionality |

View File

@@ -0,0 +1,245 @@
# Umbraco.Web.Website
Front-end website functionality for Umbraco CMS. Provides Surface controllers, member authentication/registration, content routing, and Razor view engine support for rendering published content.
**Project Type**: Class Library (NuGet package)
**Target Framework**: net10.0
**Package ID**: Umbraco.Cms.Web.Website
**Namespace**: Umbraco.Cms.Web.Website
**Dependencies**: Umbraco.Web.Common
---
## 1. Architecture
### Project Purpose
This project provides the front-end website layer:
1. **Surface Controllers** - POST/GET controllers for forms and user interactions
2. **Member Authentication** - Login, registration, profile, 2FA, external logins
3. **Content Routing** - Dynamic route value transformation for Umbraco content
4. **View Engines** - Custom Razor view locations and profiling wrappers
5. **Public Access** - Protected content with member authentication
### Folder Structure
```
Umbraco.Web.Website/
├── ActionResults/
│ ├── RedirectToUmbracoPageResult.cs # Redirect to content by key (151 lines)
│ ├── RedirectToUmbracoUrlResult.cs # Redirect to current URL
│ └── UmbracoPageResult.cs # Return current page with ViewData
├── Cache/
│ └── PartialViewCacheInvalidators/
│ └── MemberPartialViewCacheInvalidator.cs
├── Collections/
│ ├── SurfaceControllerTypeCollection.cs
│ └── SurfaceControllerTypeCollectionBuilder.cs
├── Controllers/
│ ├── SurfaceController.cs # Base front-end controller (113 lines)
│ ├── UmbLoginController.cs # Member login (128 lines)
│ ├── UmbRegisterController.cs # Member registration
│ ├── UmbProfileController.cs # Member profile management
│ ├── UmbLoginStatusController.cs # Login status checking
│ ├── UmbExternalLoginController.cs # OAuth/external login
│ ├── UmbTwoFactorLoginController.cs # 2FA authentication
│ ├── RenderNoContentController.cs # No content page
│ └── UmbracoRenderingDefaultsOptions.cs # Default rendering options
├── DependencyInjection/
│ ├── UmbracoBuilderExtensions.cs # AddWebsite() (91 lines)
│ └── UmbracoBuilder.MemberIdentity.cs # AddMemberExternalLogins() (22 lines)
├── Extensions/
│ ├── HtmlHelperRenderExtensions.cs # Razor render helpers
│ ├── LinkGeneratorExtensions.cs # URL generation
│ ├── TypeLoaderExtensions.cs # Surface controller discovery
│ ├── UmbracoApplicationBuilder.Website.cs # UseWebsite() middleware
│ └── WebsiteUmbracoBuilderExtensions.cs
├── Middleware/
│ └── BasicAuthenticationMiddleware.cs # Basic auth support
├── Models/
│ ├── LoginModel.cs # (in Web.Common)
│ ├── RegisterModel.cs # Registration form
│ ├── ProfileModel.cs # Profile form
│ ├── RegisterModelBuilder.cs # Model builder
│ ├── ProfileModelBuilder.cs # Profile builder
│ ├── MemberModelBuilderBase.cs # Builder base class
│ ├── MemberModelBuilderFactory.cs # Factory for builders
│ └── NoNodesViewModel.cs # Empty site view model
├── Routing/
│ ├── UmbracoRouteValueTransformer.cs # Dynamic route transformer (330 lines)
│ ├── UmbracoRouteValuesFactory.cs # Creates UmbracoRouteValues
│ ├── ControllerActionSearcher.cs # Finds controller actions
│ ├── PublicAccessRequestHandler.cs # Protected content handling
│ ├── FrontEndRoutes.cs # Route definitions
│ ├── EagerMatcherPolicy.cs # Early route matching
│ ├── NotFoundSelectorPolicy.cs # 404 handling
│ ├── SurfaceControllerMatcherPolicy.cs # Surface controller matching
│ ├── IControllerActionSearcher.cs
│ ├── IPublicAccessRequestHandler.cs
│ └── IUmbracoRouteValuesFactory.cs
├── Security/
│ ├── MemberAuthenticationBuilder.cs # Auth configuration
│ └── MemberExternalLoginsBuilder.cs # External login providers
├── ViewEngines/
│ ├── ProfilingViewEngine.cs # MiniProfiler wrapper
│ ├── ProfilingViewEngineWrapperMvcViewOptionsSetup.cs
│ ├── PluginRazorViewEngineOptionsSetup.cs # Plugin view locations
│ └── RenderRazorViewEngineOptionsSetup.cs # Render view locations
└── Umbraco.Web.Website.csproj # Project file (34 lines)
```
### Request Flow
```
HTTP Request → UmbracoRouteValueTransformer.TransformAsync()
Check UmbracoContext exists
Check routable document filter
RouteRequestAsync() → IPublishedRouter
UmbracoRouteValuesFactory.CreateAsync()
PublicAccessRequestHandler (protected content)
Check for Surface controller POST (ufprt)
↓ (if POST)
HandlePostedValues() → Surface Controller
↓ (else)
Return route values → Render Controller
```
---
## 2. Commands
**For Git workflow and build commands**, see [repository root](../../CLAUDE.md).
---
## 3. Key Components
### SurfaceController (Controllers/SurfaceController.cs)
Base class for front-end form controllers (113 lines).
**Key Features**:
- `[AutoValidateAntiforgeryToken]` applied by default (line 19)
- Extends `PluginController` with `IPublishedUrlProvider`
- `CurrentPage` property for accessing routed content (lines 40-53)
- Redirect helpers for content by key/entity (lines 58-102)
- Supports query strings on redirects
### UmbracoRouteValueTransformer (Routing/UmbracoRouteValueTransformer.cs)
Dynamic route value transformer for front-end content routing (330 lines).
**TransformAsync Flow** (lines 120-194):
1. Check `UmbracoContext` exists (skip client-side requests)
2. Check `IRoutableDocumentFilter.IsDocumentRequest()` (skip static files)
3. Check no existing dynamic routing active
4. Check `IDocumentUrlService.HasAny()``RenderNoContentController` if empty
5. `RouteRequestAsync()` → create published request via `IPublishedRouter`
6. `UmbracoRouteValuesFactory.CreateAsync()` → resolve controller/action
7. `PublicAccessRequestHandler.RewriteForPublishedContentAccessAsync()` → handle protected content
8. Store `UmbracoRouteValues` in `HttpContext.Features`
9. Check for Surface controller POST via `ufprt` parameter
10. Return route values for controller/action
**Surface Controller POST Handling** (lines 244-309):
- Detects `ufprt` (Umbraco Form Post Route Token) in request
- Decrypts via `EncryptionHelper.DecryptAndValidateEncryptedRouteString()`
- Extracts controller, action, area from encrypted data
- Routes to Surface controller action
**Reserved Keys for ufprt** (lines 321-327):
- `c` = Controller
- `a` = Action
- `ar` = Area
### AddWebsite() (DependencyInjection/UmbracoBuilderExtensions.cs)
Main DI registration entry point (91 lines).
**Key Registrations** (lines 34-90): Surface controller discovery, view engine setup, routing services, matcher policies, member models, distributed cache, ModelsBuilder, and member identity.
### Member Controllers
Built-in Surface controllers for member functionality:
| Controller | Purpose | Key Action |
|------------|---------|------------|
| `UmbLoginController` | Member login | `HandleLogin(LoginModel)` |
| `UmbRegisterController` | Member registration | `HandleRegister(RegisterModel)` |
| `UmbProfileController` | Profile management | `HandleUpdate(ProfileModel)` |
| `UmbLoginStatusController` | Login status | `HandleLogout()` |
| `UmbExternalLoginController` | OAuth login | `ExternalLogin(provider)` |
| `UmbTwoFactorLoginController` | 2FA verification | `HandleTwoFactorLogin()` |
### UmbLoginController (Controllers/UmbLoginController.cs)
Member login Surface controller (128 lines).
**HandleLogin** (lines 53-114): Validates credentials, handles 2FA/lockout, redirects on success. Uses encrypted redirect URLs (lines 120-126).
### RedirectToUmbracoPageResult (ActionResults/RedirectToUmbracoPageResult.cs)
Action result for redirecting to Umbraco content (151 lines).
Implements `IKeepTempDataResult` to preserve TempData. Resolves content by `Guid` key or `IPublishedContent`, supports query strings.
---
## 4. View Engine Configuration
**Custom View Locations**:
- `RenderRazorViewEngineOptionsSetup` - /Views/{controller}/{action}.cshtml
- `PluginRazorViewEngineOptionsSetup` - /App_Plugins/{area}/Views/...
- `ProfilingViewEngine` - MiniProfiler wrapper for view timing
---
## 5. Project-Specific Notes
**Warning Suppressions** (csproj lines 10-18): ASP0019, CS0618, SA1401, SA1649, IDE1006
**InternalsVisibleTo**: Umbraco.Tests.UnitTests, Umbraco.Tests.Integration
**Known Technical Debt**:
1. Load balanced setup needs review (UmbracoBuilderExtensions.cs:47)
2. UmbracoContext re-assignment pattern (UmbracoRouteValueTransformer.cs:229-232)
3. Obsolete constructor without IDocumentUrlService (removal in Umbraco 18)
**Surface Controller Form Token (ufprt)**: Encrypted route token prevents tampering. Hidden field includes controller/action/area encrypted via Data Protection, decrypted in `UmbracoRouteValueTransformer.GetFormInfo()`.
**Public Access**: `PublicAccessRequestHandler` checks member authentication, redirects to login if needed.
---
## Quick Reference
**Startup**:
```csharp
builder.CreateUmbracoBuilder().AddWebsite().AddMembersIdentity().Build();
app.UseUmbraco().WithMiddleware(u => u.UseWebsite()).WithEndpoints(u => u.UseWebsiteEndpoints());
```
### Key Interfaces
| Interface | Implementation | Purpose |
|-----------|----------------|---------|
| `IControllerActionSearcher` | ControllerActionSearcher | Find controller actions |
| `IUmbracoRouteValuesFactory` | UmbracoRouteValuesFactory | Create route values |
| `IPublicAccessRequestHandler` | PublicAccessRequestHandler | Protected content |
| `IRoutableDocumentFilter` | RoutableDocumentFilter | Filter routable requests |
### Related Projects
| Project | Relationship |
|---------|--------------|
| `Umbraco.Web.Common` | Base controllers, UmbracoContext |
| `Umbraco.Core` | Interfaces, routing contracts |
| `Umbraco.Infrastructure` | Service implementations |
| `Umbraco.Web.UI` | Main web application (references this) |