Compare commits
28
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c087ce9fb5 | ||
|
|
8dd8820fa3 | ||
|
|
6b76230da5 | ||
|
|
2e3628dad3 | ||
|
|
cb23c84c3d | ||
|
|
1dfcd9332a | ||
|
|
d84186061c | ||
|
|
87bc6522f1 | ||
|
|
c300ebf94a | ||
|
|
e2252a8634 | ||
|
|
37e2458475 | ||
|
|
2461853b11 | ||
|
|
16c97d613f | ||
|
|
cffac2a990 | ||
|
|
6883c6fcfd | ||
|
|
5dd28e7a13 | ||
|
|
87b24912c7 | ||
|
|
f6c70e8429 | ||
|
|
1dbcf1037a | ||
|
|
27909ed18e | ||
|
|
54827c4c92 | ||
|
|
214fd03241 | ||
|
|
787f66be3d | ||
|
|
cd23fc75c5 | ||
|
|
0adb5d21f5 | ||
|
|
446a3795b7 | ||
|
|
3f0e0747fa | ||
|
|
f3471e961f |
@@ -7,7 +7,7 @@ body:
|
||||
id: "version"
|
||||
attributes:
|
||||
label: "Which Umbraco version are you using?"
|
||||
description: "Please write the *exact* version, example: `10.1.0`. Click the Umbraco logo in the top left corner of the backoffice to find the version you're using."
|
||||
description: "Please write the *exact* version, example: `10.1.0`. Use the help icon in the Umbraco backoffice to find the version you're using"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
|
||||
@@ -448,14 +448,6 @@ When a PR changes Management API controllers or models, the `OpenApi.json` file
|
||||
|
||||
The backoffice is published to npm as `@umbraco-cms/backoffice`. Runtime dependencies are provided via importmap; npm peerDependencies provide types only. For full details on dependency hoisting, version range logic, and plugin development, see `/src/Umbraco.Web.UI.Client/CLAUDE.md` → "npm Package Publishing".
|
||||
|
||||
### SQL Server 2100-parameter limit
|
||||
|
||||
Any `WHERE IN (@0, @1, ...)` built from a runtime-sized collection risks hitting SQL Server's 2100-parameter ceiling and throwing `SqlException` 8003 in production.
|
||||
|
||||
Batch with `IEnumerable<T>.InGroupsOf(Constants.Sql.MaxParameterCount)` or `Database.FetchByGroups(...)` whenever the collection size is driven by user data — not just when it currently fits. Watch for products of two scaling dimensions (documents × languages, properties × versions) and config-tunable batch sizes whose defaults are safe but ceilings aren't.
|
||||
|
||||
Full guidance, safe patterns and decision rule: see `/src/Umbraco.Infrastructure/CLAUDE.md` → "Avoiding the SQL Server 2100-parameter limit".
|
||||
|
||||
### Known Limitations
|
||||
|
||||
1. **Circular Dependencies**: Avoided via `Lazy<T>` or event notifications
|
||||
@@ -552,14 +544,6 @@ Allowed, but cheap to write and cheaper to leave behind. Keep them short and tra
|
||||
|
||||
---
|
||||
|
||||
## 9. Testing Practices
|
||||
|
||||
### Tests for a bug fix must fail before the fix
|
||||
|
||||
Verify any test you add for a bug fix actually catches the bug: either write the failing test first (TDD), or temporarily revert the production change and confirm the test fails before re-applying. A test that passes both ways proves nothing. Watch for coincidental passes — default seed/sort orders can make a buggy path produce the right answer for the test's specific inputs; construct inputs so the broken and fixed behaviours give visibly different results.
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Essential Commands
|
||||
|
||||
@@ -64,4 +64,14 @@
|
||||
</_ProjectReferencesWithVersions>
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
|
||||
<!-- Workaround for https://github.com/umbraco/Umbraco-CMS/issues/23018
|
||||
Due to the amount of XML documentation in this solution, the OpenAPI XML documentation source generator produces
|
||||
too many lines of code causing a StackOverflowException when running on IIS. For that reason we disable the analyzer.
|
||||
See https://learn.microsoft.com/en-us/aspnet/core/fundamentals/openapi/openapi-comments?view=aspnetcore-10.0#disabling-xml-documentation-support -->
|
||||
<Target Name="DisableCompileTimeOpenApiXmlGenerator" BeforeTargets="CoreCompile" Condition="'$(IsPackable)' != 'false' or '$(IsTestProject)' == 'true'">
|
||||
<ItemGroup>
|
||||
<Analyzer Remove="@(Analyzer)" Condition="'%(Filename)' == 'Microsoft.AspNetCore.OpenApi.SourceGenerators'" />
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
</Project>
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
<PackageVersion Include="MailKit" Version="4.16.0" />
|
||||
<PackageVersion Include="Markdig" Version="1.1.3" />
|
||||
<PackageVersion Include="Markdown" Version="2.2.1" />
|
||||
<PackageVersion Include="MessagePack" Version="3.1.4" />
|
||||
<PackageVersion Include="MessagePack" Version="3.1.7" />
|
||||
<PackageVersion Include="MiniProfiler.AspNetCore.Mvc" Version="4.5.4" />
|
||||
<PackageVersion Include="MiniProfiler.Shared" Version="4.5.4" />
|
||||
<PackageVersion Include="ncrontab" Version="3.4.0" />
|
||||
|
||||
+43
-109
@@ -45,7 +45,7 @@ parameters:
|
||||
- name: integrationNonReleaseTestFilter
|
||||
displayName: TestFilter used for non-release type builds
|
||||
type: string
|
||||
default: "TestCategory!=LongRunning&TestCategory!=NonCritical"
|
||||
default: "--filter TestCategory!=LongRunning&TestCategory!=NonCritical"
|
||||
- name: integrationReleaseTestFilter
|
||||
displayName: TestFilter used for release type builds
|
||||
type: string
|
||||
@@ -53,7 +53,7 @@ parameters:
|
||||
- name: nonWindowsIntegrationNonReleaseTestFilter
|
||||
displayName: TestFilter used for non-release type builds on non Windows agents
|
||||
type: string
|
||||
default: "TestCategory!=LongRunning&TestCategory!=NonCritical"
|
||||
default: "--filter TestCategory!=LongRunning&TestCategory!=NonCritical"
|
||||
- name: nonWindowsIntegrationReleaseTestFilter
|
||||
displayName: TestFilter used for release type builds on non Windows agents
|
||||
type: string
|
||||
@@ -455,13 +455,13 @@ stages:
|
||||
projects: "tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj"
|
||||
testRunTitle: Integration Tests SQLite - $(Agent.OS)
|
||||
${{ if and(eq(variables['Agent.OS'],'Windows_NT'), or(variables.releaseTestFilter, parameters.forceReleaseTestFilter)) }}:
|
||||
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build'
|
||||
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build ${{parameters.integrationReleaseTestFilter}}'
|
||||
${{ elseif eq(variables['Agent.OS'],'Windows_NT') }}:
|
||||
arguments: '--filter "$(testFilter) & ${{parameters.integrationNonReleaseTestFilter}}" --configuration $(buildConfiguration) --no-build'
|
||||
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build ${{parameters.integrationNonReleaseTestFilter}}'
|
||||
${{ elseif or(variables.releaseTestFilter, parameters.forceReleaseTestFilter) }}:
|
||||
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build'
|
||||
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build ${{parameters.nonWindowsIntegrationReleaseTestFilter}}'
|
||||
${{ else }}:
|
||||
arguments: '--filter "$(testFilter) & ${{parameters.nonWindowsIntegrationNonReleaseTestFilter}}" --configuration $(buildConfiguration) --no-build'
|
||||
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build ${{parameters.nonWindowsIntegrationNonReleaseTestFilter}}'
|
||||
# Integration Tests (SQL Server)
|
||||
- job:
|
||||
timeoutInMinutes: 180
|
||||
@@ -569,13 +569,13 @@ stages:
|
||||
projects: "tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj"
|
||||
testRunTitle: Integration Tests SQL Server - $(Agent.OS)
|
||||
${{ if and(eq(variables['Agent.OS'],'Windows_NT'), or(variables.releaseTestFilter, parameters.forceReleaseTestFilter)) }}:
|
||||
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build'
|
||||
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build ${{parameters.integrationReleaseTestFilter}}'
|
||||
${{ elseif eq(variables['Agent.OS'],'Windows_NT') }}:
|
||||
arguments: '--filter "$(testFilter) & ${{parameters.integrationNonReleaseTestFilter}}" --configuration $(buildConfiguration) --no-build'
|
||||
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build ${{parameters.integrationNonReleaseTestFilter}}'
|
||||
${{ elseif or(variables.releaseTestFilter, parameters.forceReleaseTestFilter) }}:
|
||||
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build'
|
||||
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build ${{parameters.nonWindowsIntegrationReleaseTestFilter}}'
|
||||
${{ else }}:
|
||||
arguments: '--filter "$(testFilter) & ${{parameters.nonWindowsIntegrationNonReleaseTestFilter}}" --configuration $(buildConfiguration) --no-build'
|
||||
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build ${{parameters.nonWindowsIntegrationNonReleaseTestFilter}}'
|
||||
|
||||
# Stop SQL Server
|
||||
- pwsh: docker stop mssql
|
||||
@@ -825,74 +825,31 @@ stages:
|
||||
publishFeedCredentials: "MyGet - Umbraco Nightly"
|
||||
${{ else }}:
|
||||
publishFeedCredentials: "MyGet - Pre-releases"
|
||||
# Pre-release/nightly feeds: keep the `latest` dist-tag default (no `next` split).
|
||||
- job:
|
||||
displayName: Push to pre-release feed (npm)
|
||||
steps:
|
||||
- checkout: none
|
||||
- download: current
|
||||
artifact: npm
|
||||
- bash: |
|
||||
# Check if we are on a nightly build
|
||||
if [ $isNightly = "False" ]; then
|
||||
echo "##[debug]Prerelease build detected"
|
||||
registry="https://www.myget.org/F/umbracoprereleases/npm/"
|
||||
else
|
||||
echo "##[debug]Nightly build detected"
|
||||
registry="https://www.myget.org/F/umbraconightly/npm/"
|
||||
fi
|
||||
echo "@umbraco-cms:registry=$registry" >> .npmrc
|
||||
env:
|
||||
isNightly: ${{parameters.isNightly}}
|
||||
workingDirectory: $(Pipeline.Workspace)/npm
|
||||
displayName: Add scoped registry to .npmrc
|
||||
- task: npmAuthenticate@0
|
||||
displayName: Authenticate with npm (MyGet)
|
||||
inputs:
|
||||
workingFile: "$(Pipeline.Workspace)/npm/.npmrc"
|
||||
- template: templates/npm-publish.yml
|
||||
parameters:
|
||||
artifactName: npm
|
||||
customEndpoint: "MyGet (npm) - Umbracoprereleases, MyGet (npm) - Umbraconightly"
|
||||
- bash: |
|
||||
# Setup temp npm project to load in defaults from the local .npmrc
|
||||
npm init -y
|
||||
|
||||
# Find the first .tgz file in the current directory and publish it
|
||||
files=( ./*.tgz )
|
||||
npm publish "${files[0]}"
|
||||
displayName: Push to npm (MyGet)
|
||||
workingDirectory: $(Pipeline.Workspace)/npm
|
||||
displayName: Push to npm (MyGet)
|
||||
${{ if eq(parameters.isNightly, true) }}:
|
||||
registry: https://www.myget.org/F/umbraconightly/npm/
|
||||
${{ else }}:
|
||||
registry: https://www.myget.org/F/umbracoprereleases/npm/
|
||||
- job: PublishTestHelpersNpm
|
||||
displayName: Push TestHelpers to pre-release feed (npm)
|
||||
steps:
|
||||
- checkout: none
|
||||
- download: current
|
||||
artifact: npm-testhelpers
|
||||
- bash: |
|
||||
# Check if we are on a nightly build
|
||||
if [ $isNightly = "False" ]; then
|
||||
echo "##[debug]Prerelease build detected"
|
||||
registry="https://www.myget.org/F/umbracoprereleases/npm/"
|
||||
else
|
||||
echo "##[debug]Nightly build detected"
|
||||
registry="https://www.myget.org/F/umbraconightly/npm/"
|
||||
fi
|
||||
echo "@umbraco-cms:registry=$registry" >> .npmrc
|
||||
env:
|
||||
isNightly: ${{parameters.isNightly}}
|
||||
workingDirectory: $(Pipeline.Workspace)/npm-testhelpers
|
||||
displayName: Add scoped registry to .npmrc
|
||||
- task: npmAuthenticate@0
|
||||
displayName: Authenticate with npm (MyGet)
|
||||
inputs:
|
||||
workingFile: "$(Pipeline.Workspace)/npm-testhelpers/.npmrc"
|
||||
- template: templates/npm-publish.yml
|
||||
parameters:
|
||||
artifactName: npm-testhelpers
|
||||
customEndpoint: "MyGet (npm) - Umbracoprereleases, MyGet (npm) - Umbraconightly"
|
||||
- bash: |
|
||||
# Setup temp npm project to load in defaults from the local .npmrc
|
||||
npm init -y
|
||||
|
||||
# Find the first .tgz file in the current directory and publish it
|
||||
files=( ./*.tgz )
|
||||
npm publish "${files[0]}"
|
||||
displayName: Push test helpers to npm (MyGet)
|
||||
workingDirectory: $(Pipeline.Workspace)/npm-testhelpers
|
||||
displayName: Push test helpers to npm (MyGet)
|
||||
${{ if eq(parameters.isNightly, true) }}:
|
||||
registry: https://www.myget.org/F/umbraconightly/npm/
|
||||
${{ else }}:
|
||||
registry: https://www.myget.org/F/umbracoprereleases/npm/
|
||||
|
||||
- stage: Deploy_NuGet
|
||||
displayName: NuGet release
|
||||
@@ -905,10 +862,10 @@ stages:
|
||||
- job: WaitForApproval
|
||||
displayName: Wait for manual approval
|
||||
pool: server
|
||||
timeoutInMinutes: 4320 # 3 days
|
||||
steps:
|
||||
- task: ManualValidation@0
|
||||
displayName: Manual approval to push to NuGet
|
||||
timeoutInMinutes: 4320 # 3 days
|
||||
inputs:
|
||||
notifyUsers: ''
|
||||
instructions: 'Approve to push the NuGet release.'
|
||||
@@ -941,53 +898,30 @@ stages:
|
||||
condition: and(in(dependencies.Deploy_NuGet.result, 'Succeeded', 'SucceededWithIssues'), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.npmDeploy}}))
|
||||
dependsOn:
|
||||
- Deploy_NuGet
|
||||
variables:
|
||||
# `latest` for stable releases, `next` for prereleases.
|
||||
npmDistTag: $[ iif(eq(stageDependencies.Build.A.outputs['build.NBGV_PrereleaseVersionNoLeadingHyphen'], ''), 'latest', 'next') ]
|
||||
jobs:
|
||||
- job: Publish
|
||||
displayName: Push to NPM
|
||||
steps:
|
||||
- checkout: none
|
||||
- download: current
|
||||
artifact: npm
|
||||
- bash: echo "@umbraco-cms:registry=https://registry.npmjs.org" >> .npmrc
|
||||
workingDirectory: $(Pipeline.Workspace)/npm
|
||||
displayName: Add scoped registry to .npmrc
|
||||
- task: npmAuthenticate@0
|
||||
displayName: Authenticate with npm
|
||||
inputs:
|
||||
workingFile: $(Pipeline.Workspace)/npm/.npmrc
|
||||
- template: templates/npm-publish.yml
|
||||
parameters:
|
||||
artifactName: npm
|
||||
registry: https://registry.npmjs.org/
|
||||
customEndpoint: "NPM - Umbraco Backoffice"
|
||||
- script: |
|
||||
# Setup temp npm project to load in defaults from the local .npmrc
|
||||
npm init -y
|
||||
|
||||
# Find the first .tgz file in the current directory and publish it
|
||||
files=( ./*.tgz )
|
||||
npm publish "${files[0]}"
|
||||
displayName: Push to npm
|
||||
workingDirectory: $(Pipeline.Workspace)/npm
|
||||
displayName: Push to npm
|
||||
npmTag: $(npmDistTag)
|
||||
- job: PublishTestHelpers
|
||||
displayName: Push Test Helpers to NPM
|
||||
steps:
|
||||
- checkout: none
|
||||
- download: current
|
||||
artifact: npm-testhelpers
|
||||
- bash: echo "@umbraco-cms:registry=https://registry.npmjs.org" >> .npmrc
|
||||
workingDirectory: $(Pipeline.Workspace)/npm-testhelpers
|
||||
displayName: Add scoped registry to .npmrc
|
||||
- task: npmAuthenticate@0
|
||||
displayName: Authenticate with npm
|
||||
inputs:
|
||||
workingFile: $(Pipeline.Workspace)/npm-testhelpers/.npmrc
|
||||
- template: templates/npm-publish.yml
|
||||
parameters:
|
||||
artifactName: npm-testhelpers
|
||||
registry: https://registry.npmjs.org/
|
||||
customEndpoint: "NPM - Umbraco Backoffice"
|
||||
- script: |
|
||||
# Setup temp npm project to load in defaults from the local .npmrc
|
||||
npm init -y
|
||||
|
||||
# Find the first .tgz file in the current directory and publish it
|
||||
files=( ./*.tgz )
|
||||
npm publish "${files[0]}"
|
||||
displayName: Push test helpers to npm
|
||||
workingDirectory: $(Pipeline.Workspace)/npm-testhelpers
|
||||
displayName: Push test helpers to npm
|
||||
npmTag: $(npmDistTag)
|
||||
|
||||
- stage: Upload_API_Docs
|
||||
pool:
|
||||
|
||||
@@ -4,11 +4,11 @@ pr: none
|
||||
trigger: none
|
||||
|
||||
schedules:
|
||||
- cron: '0 0 * * *'
|
||||
displayName: Daily 0AM build (main)
|
||||
- cron: '0 6 * * *'
|
||||
displayName: Daily 6AM build (v18/dev)
|
||||
branches:
|
||||
include:
|
||||
- main
|
||||
- v18/dev
|
||||
|
||||
parameters:
|
||||
- name: skipIntegrationTests
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
parameters:
|
||||
- name: artifactName # "npm" or "npm-testhelpers"
|
||||
type: string
|
||||
- name: registry # scoped-registry URL to publish to
|
||||
type: string
|
||||
- name: customEndpoint # npmAuthenticate service connection(s)
|
||||
type: string
|
||||
- name: displayName # label for the publish step
|
||||
type: string
|
||||
- name: npmTag # dist-tag to publish under
|
||||
type: string
|
||||
default: latest
|
||||
|
||||
steps:
|
||||
- checkout: none
|
||||
- download: current
|
||||
artifact: ${{ parameters.artifactName }}
|
||||
- script: npm config set @umbraco-cms:registry ${{ parameters.registry }} --location=project
|
||||
displayName: Add scoped registry to .npmrc
|
||||
workingDirectory: $(Pipeline.Workspace)/${{ parameters.artifactName }}
|
||||
- task: npmAuthenticate@0
|
||||
displayName: Authenticate with npm
|
||||
inputs:
|
||||
workingFile: $(Pipeline.Workspace)/${{ parameters.artifactName }}/.npmrc
|
||||
customEndpoint: ${{ parameters.customEndpoint }}
|
||||
- script: npm publish *.tgz --tag ${{ parameters.npmTag }}
|
||||
displayName: ${{ parameters.displayName }}
|
||||
workingDirectory: $(Pipeline.Workspace)/${{ parameters.artifactName }}
|
||||
@@ -1,5 +1,7 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Schema;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
using Microsoft.AspNetCore.Http.Json;
|
||||
using Microsoft.AspNetCore.OpenApi;
|
||||
@@ -341,6 +343,12 @@ public sealed class ContentTypeSchemaTransformer : IOpenApiSchemaTransformer, IO
|
||||
|
||||
var schemaId = GetSchemaId(jsonTypeInfo);
|
||||
|
||||
// Types that produce 'true' in JSON Schema (unconstrained: JsonNode, object, custom-converter types) should be inline {} rather than named components.
|
||||
if (jsonTypeInfo.Kind == JsonTypeInfoKind.None && jsonTypeInfo.GetJsonSchemaAsNode().GetValueKind() == JsonValueKind.True)
|
||||
{
|
||||
return new OpenApiSchema();
|
||||
}
|
||||
|
||||
// If this is one of the types we handle, and we already started generating it, return a placeholder
|
||||
// to avoid circular reference issues.
|
||||
// In the document transformer, these placeholders will be replaced with the actual schemas.
|
||||
|
||||
+3
-6
@@ -53,14 +53,11 @@ public class SearchDataTypeItemController : DatatypeItemControllerBase
|
||||
return Ok(new PagedModel<DataTypeItemResponseModel> { Total = searchResult.Total });
|
||||
}
|
||||
|
||||
Guid[] keys = searchResult.Items.Select(x => x.Key).ToArray();
|
||||
IEnumerable<IDataType> dataTypes = await _dataTypeService.GetAllAsync(keys);
|
||||
IEnumerable<IDataType> orderedDataTypes = OrderByRequestedIds(dataTypes, keys);
|
||||
|
||||
IEnumerable<IDataType> dataTypes = await _dataTypeService.GetAllAsync(searchResult.Items.Select(item => item.Key).ToArray());
|
||||
var result = new PagedModel<DataTypeItemResponseModel>
|
||||
{
|
||||
Items = _mapper.MapEnumerable<IDataType, DataTypeItemResponseModel>(orderedDataTypes),
|
||||
Total = searchResult.Total,
|
||||
Items = _mapper.MapEnumerable<IDataType, DataTypeItemResponseModel>(dataTypes),
|
||||
Total = searchResult.Total
|
||||
};
|
||||
|
||||
return Ok(result);
|
||||
|
||||
+1
-2
@@ -61,9 +61,8 @@ public class SearchElementItemController : ElementItemControllerBase
|
||||
.GetAll(UmbracoObjectTypes.Element, keys)
|
||||
.OfType<IElementEntitySlim>()
|
||||
.ToArray();
|
||||
List<IElementEntitySlim> orderedElements = OrderByRequestedIds(elements, keys);
|
||||
|
||||
ElementItemResponseModel[] items = await Task.WhenAll(orderedElements.Select(_elementPresentationFactory.CreateItemResponseModelAsync));
|
||||
ElementItemResponseModel[] items = await Task.WhenAll(elements.Select(_elementPresentationFactory.CreateItemResponseModelAsync));
|
||||
|
||||
return Ok(
|
||||
new PagedModel<ElementItemResponseModel>
|
||||
|
||||
+3
-6
@@ -54,14 +54,11 @@ public class SearchMediaTypeItemController : MediaTypeItemControllerBase
|
||||
return Task.FromResult<IActionResult>(Ok(new PagedModel<MediaTypeItemResponseModel> { Total = searchResult.Total }));
|
||||
}
|
||||
|
||||
Guid[] keys = searchResult.Items.Select(item => item.Key).ToArray();
|
||||
IEnumerable<IMediaType> mediaTypes = _mediaTypeService.GetMany(keys.EmptyNull());
|
||||
IEnumerable<IMediaType> orderedMediaTypes = OrderByRequestedIds(mediaTypes, keys);
|
||||
|
||||
IEnumerable<IMediaType> mediaTypes = _mediaTypeService.GetMany(searchResult.Items.Select(item => item.Key).ToArray().EmptyNull());
|
||||
var result = new PagedModel<MediaTypeItemResponseModel>
|
||||
{
|
||||
Items = _mapper.MapEnumerable<IMediaType, MediaTypeItemResponseModel>(orderedMediaTypes),
|
||||
Total = searchResult.Total,
|
||||
Items = _mapper.MapEnumerable<IMediaType, MediaTypeItemResponseModel>(mediaTypes),
|
||||
Total = searchResult.Total
|
||||
};
|
||||
|
||||
return Task.FromResult<IActionResult>(Ok(result));
|
||||
|
||||
+3
-14
@@ -32,14 +32,6 @@ public class SearchMemberTypeItemController : MemberTypeItemControllerBase
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Searches for member type items matching the specified query, with support for pagination.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
|
||||
/// <param name="query">The search query used to filter member type items.</param>
|
||||
/// <param name="skip">The number of items to skip before starting to collect the result set (used for pagination).</param>
|
||||
/// <param name="take">The maximum number of items to return in the result set (used for pagination).</param>
|
||||
/// <returns>A task representing the asynchronous operation. The task result contains an <see cref="IActionResult"/> with a <see cref="PagedModel{MemberTypeItemResponseModel}"/> containing the search results.</returns>
|
||||
[HttpGet("search")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedModel<MemberTypeItemResponseModel>), StatusCodes.Status200OK)]
|
||||
@@ -53,14 +45,11 @@ public class SearchMemberTypeItemController : MemberTypeItemControllerBase
|
||||
return Task.FromResult<IActionResult>(Ok(new PagedModel<MemberTypeItemResponseModel> { Total = searchResult.Total }));
|
||||
}
|
||||
|
||||
Guid[] keys = searchResult.Items.Select(item => item.Key).ToArray();
|
||||
IEnumerable<IMemberType> memberTypes = _memberTypeService.GetMany(keys);
|
||||
IEnumerable<IMemberType> orderedMemberTypes = OrderByRequestedIds(memberTypes, keys);
|
||||
|
||||
IEnumerable<IMemberType> memberTypes = _memberTypeService.GetMany(searchResult.Items.Select(item => item.Key).ToArray());
|
||||
var result = new PagedModel<MemberTypeItemResponseModel>
|
||||
{
|
||||
Items = _mapper.MapEnumerable<IMemberType, MemberTypeItemResponseModel>(orderedMemberTypes),
|
||||
Total = searchResult.Total,
|
||||
Items = _mapper.MapEnumerable<IMemberType, MemberTypeItemResponseModel>(memberTypes),
|
||||
Total = searchResult.Total
|
||||
};
|
||||
|
||||
return Task.FromResult<IActionResult>(Ok(result));
|
||||
|
||||
+45
-16
@@ -8,38 +8,67 @@ using Umbraco.Cms.Core.Security;
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.RedirectUrlManagement;
|
||||
|
||||
/// <summary>
|
||||
/// Controller for setting the redirect URL tracking status. Retained for backwards compatibility only;
|
||||
/// the endpoint no longer modifies any configuration.
|
||||
/// Controller for setting the redirect URL tracking status.
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Obsolete("This controller is deprecated and no longer modifies the configuration. Set the Umbraco:CMS:WebRouting:DisableRedirectUrlTracking configuration key instead. Scheduled for removal in Umbraco 19.")]
|
||||
public class SetStatusRedirectUrlManagementController : RedirectUrlManagementControllerBase
|
||||
{
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
private readonly IConfigManipulator _configManipulator;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SetStatusRedirectUrlManagementController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="backOfficeSecurityAccessor">Ignored. Retained for binary compatibility.</param>
|
||||
/// <param name="configManipulator">Ignored. Retained for binary compatibility.</param>
|
||||
/// <param name="backOfficeSecurityAccessor">The back office security accessor.</param>
|
||||
/// <param name="configManipulator">The configuration manipulator.</param>
|
||||
public SetStatusRedirectUrlManagementController(
|
||||
#pragma warning disable IDE0060 // Remove unused parameter
|
||||
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
|
||||
IConfigManipulator configManipulator)
|
||||
#pragma warning restore IDE0060 // Remove unused parameter
|
||||
{
|
||||
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
|
||||
_configManipulator = configManipulator;
|
||||
}
|
||||
|
||||
// TODO: Consider if we should even allow this, or only allow using the appsettings
|
||||
// We generally don't want to edit the appsettings from our code.
|
||||
// But maybe there is a valid use case for doing it on the fly.
|
||||
/// <summary>
|
||||
/// Deprecated. Returns an OK response without modifying any configuration. To toggle redirect URL tracking,
|
||||
/// set the <c>Umbraco:CMS:WebRouting:DisableRedirectUrlTracking</c> configuration key instead.
|
||||
/// Sets the redirect URL tracking status.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token for the HTTP request.</param>
|
||||
/// <param name="status">The redirect status (ignored).</param>
|
||||
/// <returns>An OK result.</returns>
|
||||
/// <param name="status">The redirect status to set.</param>
|
||||
/// <returns>An OK result if successful.</returns>
|
||||
[HttpPost("status")]
|
||||
[EndpointSummary("Deprecated. No longer changes the redirect URL tracking status.")]
|
||||
[EndpointDescription("This endpoint is deprecated and no longer modifies the configuration. To toggle redirect URL tracking, set the Umbraco:CMS:WebRouting:DisableRedirectUrlTracking configuration key instead.")]
|
||||
[EndpointSummary("Sets the redirect URL tracking status.")]
|
||||
[EndpointDescription("Updates the redirect URL tracking configuration according to the provided status.")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[Obsolete("This endpoint is deprecated and no longer modifies the configuration. Set the Umbraco:CMS:WebRouting:DisableRedirectUrlTracking configuration key instead. Scheduled for removal in Umbraco 19.")]
|
||||
public Task<IActionResult> SetStatus(CancellationToken cancellationToken, [FromQuery] RedirectStatus status)
|
||||
=> Task.FromResult<IActionResult>(Ok());
|
||||
public async Task<IActionResult> SetStatus(CancellationToken cancellationToken, [FromQuery] RedirectStatus status)
|
||||
{
|
||||
// TODO: uncomment this when auth is implemented.
|
||||
// var userIsAdmin = _backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.IsAdmin();
|
||||
// if (userIsAdmin is null or false)
|
||||
// {
|
||||
// return Unauthorized();
|
||||
// }
|
||||
|
||||
var enable = status switch
|
||||
{
|
||||
RedirectStatus.Enabled => true,
|
||||
RedirectStatus.Disabled => false,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(status), status, "Unknown redirect status")
|
||||
};
|
||||
|
||||
// For now I'm not gonna change this to limit breaking, but it's weird to have a "disabled" switch,
|
||||
// since you're essentially negating the boolean from the get go,
|
||||
// it's much easier to reason with enabled = false == disabled.
|
||||
await _configManipulator.SaveDisableRedirectUrlTrackingAsync(!enable);
|
||||
|
||||
// Taken from the existing implementation in RedirectUrlManagementController
|
||||
// TODO this is ridiculous, but we need to ensure the configuration is reloaded, before this request is ended.
|
||||
// otherwise we can read the old value in GetEnableState.
|
||||
// The value is equal to JsonConfigurationSource.ReloadDelay
|
||||
Thread.Sleep(250);
|
||||
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
|
||||
+3
-14
@@ -32,14 +32,6 @@ public class SearchTemplateItemController : TemplateItemControllerBase
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Searches for template items matching the specified query, with support for pagination.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
|
||||
/// <param name="query">The search query used to filter template items.</param>
|
||||
/// <param name="skip">The number of items to skip before starting to collect the result set (used for pagination).</param>
|
||||
/// <param name="take">The maximum number of items to return in the result set (used for pagination).</param>
|
||||
/// <returns>A task representing the asynchronous operation. The task result contains an <see cref="IActionResult"/> with a <see cref="PagedModel{TemplateItemResponseModel}"/> containing the search results.</returns>
|
||||
[HttpGet("search")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedModel<TemplateItemResponseModel>), StatusCodes.Status200OK)]
|
||||
@@ -53,14 +45,11 @@ public class SearchTemplateItemController : TemplateItemControllerBase
|
||||
return Ok(new PagedModel<TemplateItemResponseModel> { Total = searchResult.Total });
|
||||
}
|
||||
|
||||
Guid[] keys = searchResult.Items.Select(x => x.Key).ToArray();
|
||||
IEnumerable<ITemplate> templates = await _templateService.GetAllAsync(keys);
|
||||
IEnumerable<ITemplate> orderedTemplates = OrderByRequestedIds(templates, keys);
|
||||
|
||||
IEnumerable<ITemplate> templates = await _templateService.GetAllAsync(searchResult.Items.Select(item => item.Key).ToArray());
|
||||
var result = new PagedModel<TemplateItemResponseModel>
|
||||
{
|
||||
Items = _mapper.MapEnumerable<ITemplate, TemplateItemResponseModel>(orderedTemplates),
|
||||
Total = searchResult.Total,
|
||||
Items = _mapper.MapEnumerable<ITemplate, TemplateItemResponseModel>(templates),
|
||||
Total = searchResult.Total
|
||||
};
|
||||
|
||||
return Ok(result);
|
||||
|
||||
+1
@@ -32,6 +32,7 @@ public class CreateTemporaryFileController : TemporaryFileControllerBase
|
||||
|
||||
[HttpPost("")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[Consumes("multipart/form-data")]
|
||||
[ProducesResponseType(StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[EndpointSummary("Creates a temporary file.")]
|
||||
|
||||
@@ -5,7 +5,6 @@ using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Hosting;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
using Umbraco.Cms.Web.Common.Hosting;
|
||||
using Umbraco.Cms.Web.Common.Middleware;
|
||||
|
||||
namespace Umbraco.Extensions;
|
||||
|
||||
@@ -69,10 +68,6 @@ public static partial class UmbracoBuilderExtensions
|
||||
builder.Services.AddSingleton<IBackOfficeEnabledMarker, BackOfficeEnabledMarker>();
|
||||
|
||||
builder.Services.AddUnique<IBackOfficePathGenerator, UmbracoBackOfficePathGenerator>();
|
||||
// Registered here rather than in AddWebComponents because the middleware depends on
|
||||
// IBackOfficePathGenerator (registered just above). DI scope validation would otherwise
|
||||
// fail in Delivery-only/Website-only bootstraps that never call AddBackOffice().
|
||||
builder.Services.AddSingleton<UmbracoBackOfficeCacheHeadersMiddleware>();
|
||||
builder.Services.AddUnique<IPhysicalFileSystem>(factory =>
|
||||
{
|
||||
var path = "~/";
|
||||
|
||||
+3
-3
@@ -28924,8 +28924,8 @@
|
||||
"tags": [
|
||||
"Redirect Management"
|
||||
],
|
||||
"summary": "Deprecated. No longer changes the redirect URL tracking status.",
|
||||
"description": "This endpoint is deprecated and no longer modifies the configuration. To toggle redirect URL tracking, set the Umbraco:CMS:WebRouting:DisableRedirectUrlTracking configuration key instead.",
|
||||
"summary": "Sets the redirect URL tracking status.",
|
||||
"description": "Updates the redirect URL tracking configuration according to the provided status.",
|
||||
"operationId": "PostRedirectManagementStatus",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -33430,7 +33430,7 @@
|
||||
"operationId": "PostTemporaryFile",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/x-www-form-urlencoded": {
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -21,7 +21,7 @@ public class ActionElementContainerDelete : IAction
|
||||
public string Alias => ActionAlias;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool ShowInNotifier => true;
|
||||
public bool ShowInNotifier => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanBePermissionAssigned => true;
|
||||
|
||||
@@ -21,7 +21,7 @@ public class ActionElementContainerMove : IAction
|
||||
public string Alias => ActionAlias;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool ShowInNotifier => true;
|
||||
public bool ShowInNotifier => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanBePermissionAssigned => true;
|
||||
|
||||
@@ -21,7 +21,7 @@ public class ActionElementContainerNew : IAction
|
||||
public string Alias => ActionAlias;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool ShowInNotifier => true;
|
||||
public bool ShowInNotifier => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanBePermissionAssigned => true;
|
||||
|
||||
@@ -21,7 +21,7 @@ public class ActionElementContainerUpdate : IAction
|
||||
public string Alias => ActionAlias;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool ShowInNotifier => true;
|
||||
public bool ShowInNotifier => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanBePermissionAssigned => true;
|
||||
|
||||
@@ -21,7 +21,7 @@ public class ActionElementCopy : IAction
|
||||
public string Alias => ActionAlias;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool ShowInNotifier => true;
|
||||
public bool ShowInNotifier => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanBePermissionAssigned => true;
|
||||
|
||||
@@ -21,7 +21,7 @@ public class ActionElementDelete : IAction
|
||||
public string Alias => ActionAlias;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool ShowInNotifier => true;
|
||||
public bool ShowInNotifier => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanBePermissionAssigned => true;
|
||||
|
||||
@@ -21,7 +21,7 @@ public class ActionElementMove : IAction
|
||||
public string Alias => ActionAlias;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool ShowInNotifier => true;
|
||||
public bool ShowInNotifier => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanBePermissionAssigned => true;
|
||||
|
||||
@@ -21,7 +21,7 @@ public class ActionElementNew : IAction
|
||||
public string Alias => ActionAlias;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool ShowInNotifier => true;
|
||||
public bool ShowInNotifier => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanBePermissionAssigned => true;
|
||||
|
||||
@@ -21,7 +21,7 @@ public class ActionElementPublish : IAction
|
||||
public string Alias => ActionAlias;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool ShowInNotifier => true;
|
||||
public bool ShowInNotifier => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanBePermissionAssigned => true;
|
||||
|
||||
@@ -21,7 +21,7 @@ public class ActionElementRollback : IAction
|
||||
public string Alias => ActionAlias;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool ShowInNotifier => true;
|
||||
public bool ShowInNotifier => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanBePermissionAssigned => true;
|
||||
|
||||
@@ -21,7 +21,7 @@ public class ActionElementUpdate : IAction
|
||||
public string Alias => ActionAlias;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool ShowInNotifier => true;
|
||||
public bool ShowInNotifier => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanBePermissionAssigned => true;
|
||||
|
||||
@@ -306,8 +306,6 @@ public class MyEntityCacheRefresher : CacheRefresherBase<MyEntityCacheRefresher>
|
||||
- `Attempt.Succeed(value)` / `Attempt.Fail<T>()`
|
||||
- `Attempt<Content, ContentEditingOperationStatus>` - typed result with status
|
||||
|
||||
> Writing or reviewing a query with a `WHERE IN` on a runtime-sized collection? See "Avoiding the SQL Server 2100-parameter limit" in `/src/Umbraco.Infrastructure/CLAUDE.md` — that's where the full helper list (`Constants.Sql.MaxParameterCount`, `InGroupsOf`, NPoco's `FetchByGroups`) and the decision rules live.
|
||||
|
||||
### Configuration
|
||||
|
||||
Configuration models in `/Configuration/Models`:
|
||||
|
||||
@@ -467,6 +467,20 @@ public static class DistributedCacheExtensions
|
||||
|
||||
#endregion
|
||||
|
||||
#region ElementContainerCacheRefresher
|
||||
|
||||
/// <summary>
|
||||
/// Invalidates the id/key map for the specified deleted element containers (folders).
|
||||
/// </summary>
|
||||
/// <param name="dc">The distributed cache.</param>
|
||||
/// <param name="deletedContainers">The element containers that were deleted.</param>
|
||||
public static void RemoveElementContainerCache(this DistributedCache dc, IEnumerable<EntityContainer> deletedContainers)
|
||||
=> dc.RefreshByPayload(
|
||||
ElementContainerCacheRefresher.UniqueId,
|
||||
deletedContainers.Select(container => new ElementContainerCacheRefresher.JsonPayload(container.Id, container.Key)));
|
||||
|
||||
#endregion
|
||||
|
||||
#region Published Snapshot
|
||||
|
||||
/// <summary>
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.Cache;
|
||||
|
||||
/// <summary>
|
||||
/// Invalidates element caches when an element container (folder) is deleted, so that its key→id mapping
|
||||
/// is evicted from <see cref="Services.IIdKeyMap"/> on every server.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Element container deletions only publish <see cref="EntityContainerDeletedNotification"/> and an
|
||||
/// <see cref="ElementTreeChangeNotification"/> for the contained elements - never for the container node
|
||||
/// itself, so without this handler the container's stale id/key mapping survives until the next app
|
||||
/// restart (see #23072).
|
||||
/// </remarks>
|
||||
public sealed class ElementContainerDeletedDistributedCacheNotificationHandler
|
||||
: DeletedDistributedCacheNotificationHandlerBase<EntityContainer, EntityContainerDeletedNotification>
|
||||
{
|
||||
private readonly DistributedCache _distributedCache;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ElementContainerDeletedDistributedCacheNotificationHandler"/> class.
|
||||
/// </summary>
|
||||
/// <param name="distributedCache">The distributed cache.</param>
|
||||
public ElementContainerDeletedDistributedCacheNotificationHandler(DistributedCache distributedCache)
|
||||
=> _distributedCache = distributedCache;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Handle(IEnumerable<EntityContainer> entities, IDictionary<string, object?> state)
|
||||
{
|
||||
EntityContainer[] elementContainers = entities
|
||||
.Where(container => container.ContainerObjectType == Constants.ObjectTypes.ElementContainer)
|
||||
.ToArray();
|
||||
|
||||
if (elementContainers.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_distributedCache.RemoveElementContainerCache(elementContainers);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Core.Cache;
|
||||
|
||||
/// <summary>
|
||||
/// Provides cache refresh functionality for element containers (folders).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A deleted container's node id is never reused, so its key→id mapping in <see cref="IIdKeyMap"/> must be
|
||||
/// evicted on every server. Otherwise a container recreated under the same key resolves to the stale id and
|
||||
/// the element tree's children query returns nothing until the next app restart. This refresher only evicts
|
||||
/// the id/key map - element data is unaffected by container changes, so it deliberately avoids the broader
|
||||
/// invalidation performed by <see cref="ElementCacheRefresher"/>.
|
||||
/// </remarks>
|
||||
public sealed class ElementContainerCacheRefresher : PayloadCacheRefresherBase<ElementContainerCacheRefresherNotification, ElementContainerCacheRefresher.JsonPayload>
|
||||
{
|
||||
private readonly IIdKeyMap _idKeyMap;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ElementContainerCacheRefresher"/> class.
|
||||
/// </summary>
|
||||
public ElementContainerCacheRefresher(
|
||||
AppCaches appCaches,
|
||||
IJsonSerializer serializer,
|
||||
IIdKeyMap idKeyMap,
|
||||
IEventAggregator eventAggregator,
|
||||
ICacheRefresherNotificationFactory factory)
|
||||
: base(appCaches, serializer, eventAggregator, factory)
|
||||
=> _idKeyMap = idKeyMap;
|
||||
|
||||
#region Json
|
||||
|
||||
/// <summary>
|
||||
/// Represents a JSON-serializable payload identifying an element container that changed.
|
||||
/// </summary>
|
||||
public class JsonPayload
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="JsonPayload"/> class.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique integer identifier for the container.</param>
|
||||
/// <param name="key">The unique GUID key associated with the container.</param>
|
||||
public JsonPayload(int id, Guid key)
|
||||
{
|
||||
Id = id;
|
||||
Key = key;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the unique integer identifier for the container.
|
||||
/// </summary>
|
||||
public int Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the unique GUID key associated with the container.
|
||||
/// </summary>
|
||||
public Guid Key { get; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Define
|
||||
|
||||
/// <summary>
|
||||
/// Represents a unique identifier for the cache refresher.
|
||||
/// </summary>
|
||||
public static readonly Guid UniqueId = Guid.Parse("9C9D8B0E-2F1A-4D63-9C2E-7E6B5A4F3C21");
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override Guid RefresherUniqueId => UniqueId;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string Name => "Element Container Cache Refresher";
|
||||
|
||||
#endregion
|
||||
|
||||
#region Refresher
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Refresh(JsonPayload[] payloads)
|
||||
{
|
||||
foreach (JsonPayload payload in payloads)
|
||||
{
|
||||
// Clearing by id also evicts the key→id direction, as the id/key map keeps both in sync.
|
||||
_idKeyMap.ClearCache(payload.Id);
|
||||
}
|
||||
|
||||
base.Refresh(payloads);
|
||||
}
|
||||
|
||||
// These events should never trigger. Everything should be PAYLOAD/JSON.
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void RefreshAll() => throw new NotSupportedException();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Refresh(int id) => throw new NotSupportedException();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Refresh(Guid id) => throw new NotSupportedException();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Remove(int id) => throw new NotSupportedException();
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -36,7 +36,6 @@ public interface IConfigManipulator
|
||||
/// </summary>
|
||||
/// <param name="disable">The value to save.</param>
|
||||
/// <returns></returns>
|
||||
[Obsolete("This method is no longer used by Umbraco. Set the Umbraco:CMS:WebRouting:DisableRedirectUrlTracking configuration key instead. Scheduled for removal in Umbraco 19.")]
|
||||
Task SaveDisableRedirectUrlTrackingAsync(bool disable);
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -405,7 +405,8 @@
|
||||
0: Comma delimitted list of failed folder paths
|
||||
-->
|
||||
<key alias="umbracoApplicationUrlCheckResultTrue"><![CDATA[AppSetting 'Umbraco:CMS:WebRouting:UmbracoApplicationUrl' je postavljen na <strong>%0%</strong>.]]></key>
|
||||
<key alias="umbracoApplicationUrlCheckResultFalse">AppSetting 'Umbraco:CMS:WebRouting:UmbracoApplicationUrl' nije postavljen.</key>
|
||||
<key alias="umbracoApplicationUrlCheckResultFalse"><![CDATA[AppSetting 'Umbraco:CMS:WebRouting:UmbracoApplicationUrl' nije postavljen, pa će se URL aplikacije automatski otkriti iz dolaznih zahtjeva. Preporučuje se da ga postavite izričito.]]></key>
|
||||
<key alias="umbracoApplicationUrlCheckResultError"><![CDATA[AppSetting 'Umbraco:CMS:WebRouting:UmbracoApplicationUrl' nije postavljen, a automatsko otkrivanje URL-a aplikacije je onemogućeno ('Umbraco:CMS:WebRouting:ApplicationUrlDetection' je 'None'). Značajke koje zahtijevaju apsolutni URL, poput e-pošte za poništavanje lozinke i pozivnica, neće raditi. Postavite URL aplikacije izričito ili omogućite automatsko otkrivanje.]]></key>
|
||||
<!-- The following key get these tokens passed in:
|
||||
0: Comma delimitted list of headers found
|
||||
-->
|
||||
|
||||
@@ -454,7 +454,8 @@
|
||||
<key alias="httpsCheckConfigurationRectifyNotPossible">Mae gosodiad ap 'Umbraco:CMS:Global:UseHttps' wedi'i osod i 'false' yn eich ffeil appSettings.json. Unwaith y byddwch yn cyrchu'r wefan hon gan ddefnyddio'r cynllun HTTPS, dylid gosod hwnnw i 'true'.</key>
|
||||
<key alias="httpsCheckConfigurationCheckResult">Mae'r gosodiad ap 'Umbraco:CMS:Global:UseHttps' wedi'i osod i '%0%' yn eich ffeil appSettings.json, mae eich cwcis %1% wedi'u marcio'n ddiogel.</key>
|
||||
<key alias="umbracoApplicationUrlCheckResultTrue">Mae gosodiad yr ap 'Umbraco:CMS:WebRouting:UmbracoApplicationUrl' wedi'i osod i <strong>%0%</strong>.</key>
|
||||
<key alias="umbracoApplicationUrlCheckResultFalse">Nid yw gosodiad ap 'Umbraco:CMS:WebRouting:UmbracoApplicationUrl' wedi'i osod.</key>
|
||||
<key alias="umbracoApplicationUrlCheckResultFalse"><![CDATA[Nid yw gosodiad ap 'Umbraco:CMS:WebRouting:UmbracoApplicationUrl' wedi'i osod, felly bydd URL y rhaglen yn cael ei ganfod yn awtomatig o geisiadau sy'n dod i mewn. Argymhellir ei osod yn benodol.]]></key>
|
||||
<key alias="umbracoApplicationUrlCheckResultError"><![CDATA[Nid yw gosodiad ap 'Umbraco:CMS:WebRouting:UmbracoApplicationUrl' wedi'i osod ac mae canfod URL y rhaglen yn awtomatig wedi'i analluogi (mae 'Umbraco:CMS:WebRouting:ApplicationUrlDetection' yn 'None'). Ni fydd nodweddion sydd angen URL absoliwt, fel e-byst ailosod cyfrinair a gwahoddiadau, yn gweithio. Gosodwch URL y rhaglen yn benodol, neu galluogwch ganfod yn awtomatig.]]></key>
|
||||
<key alias="smtpMailSettingsNotFound">Nid oedd modd dod o hyd i'r ffurfweddiad 'Umbraco:CMS:Global:Smtp'.</key>
|
||||
<key alias="smtpMailSettingsHostNotConfigured">Nid oedd modd dod o hyd i'r ffurfweddiad 'Umbraco:CMS:Global:Smtp:Host'.</key>
|
||||
<key alias="smtpMailSettingsConnectionFail">Methwyd cyrraedd y gweinydd SMTP a ffurfweddwyd gyda gwesteiwr '%0%' a phorth '%1%'. Gwiriwch i sicrhau bod y gosodiadau SMTP yn y ffurfweddiad 'Umbraco:CMS:Global:Smtp' yn gywir.</key>
|
||||
|
||||
@@ -463,7 +463,8 @@
|
||||
0: Comma delimitted list of failed folder paths
|
||||
-->
|
||||
<key alias="umbracoApplicationUrlCheckResultTrue"><![CDATA[The appSetting 'Umbraco:CMS:WebRouting:UmbracoApplicationUrl' is set to <strong>%0%</strong>.]]></key>
|
||||
<key alias="umbracoApplicationUrlCheckResultFalse">The appSetting 'Umbraco:CMS:WebRouting:UmbracoApplicationUrl' is not set.</key>
|
||||
<key alias="umbracoApplicationUrlCheckResultFalse"><![CDATA[The appSetting 'Umbraco:CMS:WebRouting:UmbracoApplicationUrl' is not set, so the application URL will be auto-detected from incoming requests. Setting it explicitly is recommended.]]></key>
|
||||
<key alias="umbracoApplicationUrlCheckResultError"><![CDATA[The appSetting 'Umbraco:CMS:WebRouting:UmbracoApplicationUrl' is not set and application URL auto-detection is disabled ('Umbraco:CMS:WebRouting:ApplicationUrlDetection' is 'None'). Features that require an absolute URL, such as password reset and invitation emails, will not work. Set the application URL explicitly, or enable auto-detection.]]></key>
|
||||
<!-- The following key get these tokens passed in:
|
||||
0: Comma delimitted list of headers found
|
||||
-->
|
||||
|
||||
@@ -452,7 +452,8 @@
|
||||
0: Comma delimitted list of failed folder paths
|
||||
-->
|
||||
<key alias="umbracoApplicationUrlCheckResultTrue"><![CDATA[The appSetting 'Umbraco:CMS:WebRouting:UmbracoApplicationUrl' is set to <strong>%0%</strong>.]]></key>
|
||||
<key alias="umbracoApplicationUrlCheckResultFalse">The appSetting 'Umbraco:CMS:WebRouting:UmbracoApplicationUrl' is not set.</key>
|
||||
<key alias="umbracoApplicationUrlCheckResultFalse"><![CDATA[The appSetting 'Umbraco:CMS:WebRouting:UmbracoApplicationUrl' is not set, so the application URL will be auto-detected from incoming requests. Setting it explicitly is recommended.]]></key>
|
||||
<key alias="umbracoApplicationUrlCheckResultError"><![CDATA[The appSetting 'Umbraco:CMS:WebRouting:UmbracoApplicationUrl' is not set and application URL auto-detection is disabled ('Umbraco:CMS:WebRouting:ApplicationUrlDetection' is 'None'). Features that require an absolute URL, such as password reset and invitation emails, will not work. Set the application URL explicitly, or enable auto-detection.]]></key>
|
||||
<key alias="clickJackingCheckHeaderFound">
|
||||
<![CDATA[The header or meta-tag <strong>X-Frame-Options</strong> used to control whether a site can be IFRAMEd by another was found.]]></key>
|
||||
<key alias="clickJackingCheckHeaderNotFound">
|
||||
|
||||
@@ -403,7 +403,8 @@
|
||||
0: Comma delimitted list of failed folder paths
|
||||
-->
|
||||
<key alias="umbracoApplicationUrlCheckResultTrue"><![CDATA[AppSetting 'Umbraco:CMS:WebRouting:UmbracoApplicationUrl' je postavljen na <strong>%0%</strong>.]]></key>
|
||||
<key alias="umbracoApplicationUrlCheckResultFalse">AppSetting 'Umbraco:CMS:WebRouting:UmbracoApplicationUrl' nije postavljen.</key>
|
||||
<key alias="umbracoApplicationUrlCheckResultFalse"><![CDATA[AppSetting 'Umbraco:CMS:WebRouting:UmbracoApplicationUrl' nije postavljen, pa će se URL aplikacije automatski otkriti iz dolaznih zahtjeva. Preporučuje se da ga postavite izričito.]]></key>
|
||||
<key alias="umbracoApplicationUrlCheckResultError"><![CDATA[AppSetting 'Umbraco:CMS:WebRouting:UmbracoApplicationUrl' nije postavljen, a automatsko otkrivanje URL-a aplikacije je onemogućeno ('Umbraco:CMS:WebRouting:ApplicationUrlDetection' je 'None'). Značajke koje zahtijevaju apsolutni URL, poput e-pošte za poništavanje lozinke i pozivnica, neće raditi. Postavite URL aplikacije izričito ili omogućite automatsko otkrivanje.]]></key>
|
||||
<!-- The following key get these tokens passed in:
|
||||
0: Comma delimitted list of headers found
|
||||
-->
|
||||
|
||||
@@ -44,28 +44,34 @@ public class UmbracoApplicationUrlCheck : HealthCheck
|
||||
|
||||
private HealthCheckStatus CheckUmbracoApplicationUrl()
|
||||
{
|
||||
var url = _webRoutingSettings.CurrentValue.UmbracoApplicationUrl;
|
||||
WebRoutingSettings settings = _webRoutingSettings.CurrentValue;
|
||||
var url = settings.UmbracoApplicationUrl;
|
||||
|
||||
string resultMessage;
|
||||
StatusResultType resultType;
|
||||
var success = false;
|
||||
|
||||
if (url.IsNullOrWhiteSpace())
|
||||
if (url.IsNullOrWhiteSpace() is false)
|
||||
{
|
||||
resultMessage = _textService.Localize("healthcheck", "umbracoApplicationUrlCheckResultFalse");
|
||||
resultType = StatusResultType.Warning;
|
||||
resultMessage = _textService.Localize("healthcheck", "umbracoApplicationUrlCheckResultTrue", [url]);
|
||||
resultType = StatusResultType.Success;
|
||||
}
|
||||
else if (settings.ApplicationUrlDetection == ApplicationUrlDetection.None)
|
||||
{
|
||||
// No explicit URL and auto-detection is disabled, so the application URL can never be established.
|
||||
// Features that require an absolute URL (e.g. password reset and invitation emails) will not work.
|
||||
resultMessage = _textService.Localize("healthcheck", "umbracoApplicationUrlCheckResultError");
|
||||
resultType = StatusResultType.Error;
|
||||
}
|
||||
else
|
||||
{
|
||||
resultMessage = _textService.Localize("healthcheck", "umbracoApplicationUrlCheckResultTrue", new[] { url });
|
||||
resultType = StatusResultType.Success;
|
||||
success = true;
|
||||
resultMessage = _textService.Localize("healthcheck", "umbracoApplicationUrlCheckResultFalse");
|
||||
resultType = StatusResultType.Warning;
|
||||
}
|
||||
|
||||
return new HealthCheckStatus(resultMessage)
|
||||
{
|
||||
ResultType = resultType,
|
||||
ReadMoreLink = success
|
||||
ReadMoreLink = resultType == StatusResultType.Success
|
||||
? null
|
||||
: Constants.HealthChecks.DocumentationLinks.Security.UmbracoApplicationUrlCheck,
|
||||
};
|
||||
|
||||
@@ -106,7 +106,7 @@ public interface IHostingEnvironment
|
||||
/// content root are the same, however
|
||||
/// in netcore the web root is /www therefore this will Map to a physical path within www.
|
||||
/// </remarks>
|
||||
[Obsolete("Please use the MapPathWebRoot extension method on an instance of IWebHostEnvironment instead. Scheduled for removal in Umbraco 20.")]
|
||||
[Obsolete("Please use the MapPathWebRoot extension method on an instance of IWebHostEnvironment instead")]
|
||||
string MapPathWebRoot(string path);
|
||||
|
||||
/// <summary>
|
||||
@@ -118,7 +118,7 @@ public interface IHostingEnvironment
|
||||
/// in netcore the web root is /www therefore this will Map to a physical path within www.
|
||||
/// </remarks>
|
||||
[Obsolete(
|
||||
"Please use the MapPathContentRoot extension method on an instance of IHostEnvironment (or IWebHostEnvironment) instead. Scheduled for removal in Umbraco 20.")]
|
||||
"Please use the MapPathContentRoot extension method on an instance of IHostEnvironment (or IWebHostEnvironment) instead")]
|
||||
string MapPathContentRoot(string path);
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -64,8 +64,4 @@ public class BlockGridLayoutItem : BlockLayoutItemBase
|
||||
/// <inheritdoc />
|
||||
public override bool ReferencesSetting(Guid key)
|
||||
=> SettingsKey == key || Areas.Any(area => area.ContainsSetting(key));
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IEnumerable<IBlockLayoutItem> GetContainedLayouts()
|
||||
=> Areas.SelectMany(area => area.Items);
|
||||
}
|
||||
|
||||
@@ -5,18 +5,12 @@ namespace Umbraco.Cms.Core.Models.Blocks;
|
||||
/// </summary>
|
||||
public abstract class BlockLayoutItemBase : IBlockLayoutItem
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public Guid Key { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid ContentKey { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid? SettingsKey { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsExternalContent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BlockLayoutItemBase" /> class.
|
||||
/// </summary>
|
||||
@@ -50,7 +44,4 @@ public abstract class BlockLayoutItemBase : IBlockLayoutItem
|
||||
/// <inheritdoc />
|
||||
public virtual bool ReferencesSetting(Guid key)
|
||||
=> SettingsKey == key;
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual IEnumerable<IBlockLayoutItem> GetContainedLayouts() => [];
|
||||
}
|
||||
|
||||
@@ -8,18 +8,6 @@ namespace Umbraco.Cms.Core.Models.Blocks;
|
||||
/// </summary>
|
||||
public interface IBlockLayoutItem
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the layout item key.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The layout item key.
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// Uniquely identifies a layout item. Previously the <see cref="ContentKey"/> could be used for this, but
|
||||
/// with reusable elements, the same <see cref="ContentKey"/> can appear multiple times in one layout.
|
||||
/// </remarks>
|
||||
public Guid Key { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the content key.
|
||||
/// </summary>
|
||||
@@ -36,11 +24,6 @@ public interface IBlockLayoutItem
|
||||
/// </value>
|
||||
public Guid? SettingsKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates if the content source is local or originates from the element service.
|
||||
/// </summary>
|
||||
public bool IsExternalContent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether this layout item references the specified content key.
|
||||
/// </summary>
|
||||
@@ -58,10 +41,4 @@ public interface IBlockLayoutItem
|
||||
/// <c>true</c> if this layout item references the specified settings key; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
public bool ReferencesSetting(Guid key) => SettingsKey == key;
|
||||
|
||||
/// <summary>
|
||||
/// Returns any nested layouts for this layout (e.g. area layouts for the Block Grid).
|
||||
/// </summary>
|
||||
/// <returns>The nested layouts.</returns>
|
||||
public IEnumerable<IBlockLayoutItem> GetContainedLayouts();
|
||||
}
|
||||
|
||||
@@ -24,4 +24,9 @@ public enum TaggableObjectTypes
|
||||
/// Represents member entities (user accounts).
|
||||
/// </summary>
|
||||
Member,
|
||||
|
||||
/// <summary>
|
||||
/// Represents element entities.
|
||||
/// </summary>
|
||||
Element,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
using Umbraco.Cms.Core.Sync;
|
||||
|
||||
namespace Umbraco.Cms.Core.Notifications;
|
||||
|
||||
/// <summary>
|
||||
/// A notification that is used to trigger the Element Container Cache Refresher.
|
||||
/// </summary>
|
||||
public class ElementContainerCacheRefresherNotification : CacheRefresherNotification
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ElementContainerCacheRefresherNotification"/> class.
|
||||
/// </summary>
|
||||
/// <param name="messageObject">The refresher payload.</param>
|
||||
/// <param name="messageType">Type of the cache refresher message, <see cref="MessageType"/>.</param>
|
||||
public ElementContainerCacheRefresherNotification(object messageObject, MessageType messageType)
|
||||
: base(messageObject, messageType)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a property index value factory specifically for block list properties.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This marker interface allows for specialized indexing of block list content.
|
||||
/// </remarks>
|
||||
public interface IBlockListPropertyIndexValueFactory : IPropertyIndexValueFactory
|
||||
{
|
||||
}
|
||||
+4
-3
@@ -1,11 +1,12 @@
|
||||
namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a property index value factory specifically for block grid properties.
|
||||
/// Represents a property index value factory specifically for block-based property values.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This marker interface allows for specialized indexing of block grid content.
|
||||
/// This marker interface allows for specialized indexing of block content,
|
||||
/// such as Block List, Block Grid, and Rich Text block values.
|
||||
/// </remarks>
|
||||
public interface IBlockGridPropertyIndexValueFactory : IPropertyIndexValueFactory
|
||||
public interface IBlockValuePropertyIndexValueFactory : IPropertyIndexValueFactory
|
||||
{
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a property index value factory specifically for single block properties.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This marker interface allows for specialized indexing of single block content.
|
||||
/// </remarks>
|
||||
public interface ISingleBlockPropertyIndexValueFactory : IPropertyIndexValueFactory
|
||||
{
|
||||
}
|
||||
@@ -3,17 +3,7 @@ using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
|
||||
namespace Umbraco.Cms.Core.PublishedCache;
|
||||
|
||||
/// <summary>
|
||||
/// A service for converting <see cref="BlockItemData"/> into <see cref="IPublishedElement"/>.
|
||||
/// </summary>
|
||||
public interface IBlockElementService
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates an <see cref="IPublishedElement"/> instance from <see cref="BlockItemData"/>.
|
||||
/// </summary>
|
||||
/// <param name="owner">The <see cref="IPublishedElement"/> that contains the block property which is the origin to the <see cref="BlockItemData"/>.</param>
|
||||
/// <param name="blockItemData">The <see cref="BlockItemData"/> containing the data to convert into an <see cref="IPublishedElement"/>.</param>
|
||||
/// <param name="preview">Whether to perform the conversion for preview.</param>
|
||||
/// <returns>The created <see cref="IPublishedElement"/>, or null if an element could not be created from the <see cref="BlockItemData"/>.</returns>
|
||||
Task<IPublishedElement?> BuildElementAsync(IPublishedElement owner, BlockItemData blockItemData, bool? preview = null);
|
||||
Task<IPublishedElement?> BuildElementAsync(BlockItemData blockItemData, bool? preview = null);
|
||||
}
|
||||
|
||||
@@ -54,6 +54,18 @@ public interface ITagService : IService
|
||||
/// </summary>
|
||||
IEnumerable<TaggedEntity> GetTaggedMembersByTag(string tag, string? group = null, string? culture = null);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all elements tagged with any tag in the specified group.
|
||||
/// </summary>
|
||||
// TODO (V19): Remove the default implementation from this interface.
|
||||
IEnumerable<TaggedEntity> GetTaggedElementsByTagGroup(string group, string? culture = null) => [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets all elements tagged with the specified tag.
|
||||
/// </summary>
|
||||
// TODO (V19): Remove the default implementation from this interface.
|
||||
IEnumerable<TaggedEntity> GetTaggedElementsByTag(string tag, string? group = null, string? culture = null) => [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets all tags.
|
||||
/// </summary>
|
||||
@@ -100,6 +112,12 @@ public interface ITagService : IService
|
||||
/// </summary>
|
||||
IEnumerable<ITag> GetAllMemberTags(string? group = null, string? culture = null);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all element tags.
|
||||
/// </summary>
|
||||
// TODO (V19): Remove the default implementation from this interface.
|
||||
IEnumerable<ITag> GetAllElementTags(string? group = null, string? culture = null) => [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets all tags attached to an entity via a property.
|
||||
/// </summary>
|
||||
|
||||
@@ -101,6 +101,24 @@ public class TagService : RepositoryService, ITagService
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<TaggedEntity> GetTaggedElementsByTagGroup(string group, string? culture = null)
|
||||
{
|
||||
using (ScopeProvider.CreateCoreScope(autoComplete: true))
|
||||
{
|
||||
return _tagRepository.GetTaggedEntitiesByTagGroup(TaggableObjectTypes.Element, group, culture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<TaggedEntity> GetTaggedElementsByTag(string tag, string? group = null, string? culture = null)
|
||||
{
|
||||
using (ScopeProvider.CreateCoreScope(autoComplete: true))
|
||||
{
|
||||
return _tagRepository.GetTaggedEntitiesByTag(TaggableObjectTypes.Element, tag, group, culture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<ITag> GetAllTags(string? group = null, string? culture = null)
|
||||
{
|
||||
@@ -162,6 +180,15 @@ public class TagService : RepositoryService, ITagService
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<ITag> GetAllElementTags(string? group = null, string? culture = null)
|
||||
{
|
||||
using (ScopeProvider.CreateCoreScope(autoComplete: true))
|
||||
{
|
||||
return _tagRepository.GetTagsForEntityType(TaggableObjectTypes.Element, group, culture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<ITag> GetTagsForProperty(int contentId, string propertyTypeAlias, string? group = null, string? culture = null)
|
||||
{
|
||||
|
||||
@@ -54,7 +54,7 @@ public static class UmbracoBuilderExtensions
|
||||
.DeliveryApiContentIndexName)
|
||||
.ConfigureOptions<ConfigureIndexOptions>();
|
||||
|
||||
services.AddSingleton<IApplicationRoot>(sp => ActivatorUtilities.CreateInstance<UmbracoApplicationRoot>(sp));
|
||||
services.AddSingleton<IApplicationRoot, UmbracoApplicationRoot>();
|
||||
services.AddSingleton<ILockFactory, UmbracoLockFactory>();
|
||||
services.AddSingleton<ConfigurationEnabledDirectoryFactory>();
|
||||
|
||||
|
||||
@@ -55,9 +55,7 @@ public class LuceneIndexDiagnostics : IIndexDiagnostics
|
||||
Directory luceneDir = Index.GetLuceneDirectory();
|
||||
var d = new Dictionary<string, object?>
|
||||
{
|
||||
#pragma warning disable CS0618 // CommitCount is obsolete and reported unused, but retained to avoid any risk of change to existing behaviour. Remove this entry when a future Examine upgrade removes the underlying field.
|
||||
[nameof(UmbracoExamineIndex.CommitCount)] = Index.CommitCount,
|
||||
#pragma warning restore CS0618
|
||||
[nameof(UmbracoExamineIndex.DefaultAnalyzer)] = Index.DefaultAnalyzer.GetType().Name,
|
||||
["LuceneDirectory"] = luceneDir.GetType().Name
|
||||
};
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
using Examine;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Extensions;
|
||||
using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment;
|
||||
using Umbraco.Cms.Core.Hosting;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Examine;
|
||||
|
||||
@@ -13,24 +9,14 @@ namespace Umbraco.Cms.Infrastructure.Examine;
|
||||
/// </summary>
|
||||
public class UmbracoApplicationRoot : IApplicationRoot
|
||||
{
|
||||
private readonly IHostEnvironment _hostEnvironment;
|
||||
private readonly IHostingEnvironment _hostingEnvironment;
|
||||
|
||||
// TODO (V20): Remove this obsolete constructor and the [ActivatorUtilitiesConstructor] attribute below.
|
||||
// Also revert the registration in UmbracoBuilderExtensions to:
|
||||
// services.AddSingleton<IApplicationRoot, UmbracoApplicationRoot>();
|
||||
// (the factory form is required so [ActivatorUtilitiesConstructor] is honored).
|
||||
[Obsolete("Use the constructor accepting IHostEnvironment. Scheduled for removal in Umbraco 20.")]
|
||||
public UmbracoApplicationRoot(IHostingEnvironment hostingEnvironment)
|
||||
: this(StaticServiceProvider.Instance.GetRequiredService<IHostEnvironment>())
|
||||
{
|
||||
}
|
||||
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public UmbracoApplicationRoot(IHostEnvironment hostEnvironment)
|
||||
=> _hostEnvironment = hostEnvironment;
|
||||
=> _hostingEnvironment = hostingEnvironment;
|
||||
|
||||
public DirectoryInfo ApplicationRoot
|
||||
=> new(Path.Combine(
|
||||
_hostEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempData),
|
||||
"ExamineIndexes"));
|
||||
=> new(
|
||||
Path.Combine(
|
||||
_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempData),
|
||||
"ExamineIndexes"));
|
||||
}
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
using Examine;
|
||||
using Examine.Lucene;
|
||||
using Examine.Lucene.Directories;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Examine
|
||||
@@ -13,25 +9,11 @@ namespace Umbraco.Cms.Infrastructure.Examine
|
||||
/// </summary>
|
||||
public class UmbracoTempEnvFileSystemDirectoryFactory : FileSystemDirectoryFactory
|
||||
{
|
||||
[Obsolete("Use the constructor accepting IOptionsMonitor<LuceneDirectoryIndexOptions>. Scheduled for removal in Umbraco 20.")]
|
||||
public UmbracoTempEnvFileSystemDirectoryFactory(
|
||||
IApplicationIdentifier applicationIdentifier,
|
||||
ILockFactory lockFactory,
|
||||
IHostingEnvironment hostingEnvironment)
|
||||
: this(
|
||||
applicationIdentifier,
|
||||
lockFactory,
|
||||
hostingEnvironment,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IOptionsMonitor<LuceneDirectoryIndexOptions>>())
|
||||
{
|
||||
}
|
||||
|
||||
public UmbracoTempEnvFileSystemDirectoryFactory(
|
||||
IApplicationIdentifier applicationIdentifier,
|
||||
ILockFactory lockFactory,
|
||||
IHostingEnvironment hostingEnvironment,
|
||||
IOptionsMonitor<LuceneDirectoryIndexOptions> indexOptions)
|
||||
: base(new DirectoryInfo(GetTempPath(applicationIdentifier, hostingEnvironment)), lockFactory, indexOptions)
|
||||
: base(new DirectoryInfo(GetTempPath(applicationIdentifier, hostingEnvironment)), lockFactory)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
+13
-2
@@ -84,8 +84,19 @@ public class TouchServerJob : RecurringBackgroundJobBase
|
||||
var serverAddress = _hostingEnvironment.ApplicationMainUrl?.ToString();
|
||||
if (string.IsNullOrWhiteSpace(serverAddress))
|
||||
{
|
||||
_logger.LogWarning("No umbracoApplicationUrl for service (yet), skip.");
|
||||
return Task.CompletedTask;
|
||||
// No application URL is known yet: either detection is off (WebRouting:ApplicationUrlDetection is
|
||||
// None with no UmbracoApplicationUrl set), or detection is on but no request has been served yet.
|
||||
// Register with the machine name as a placeholder so server-role election can still proceed (uniqueness
|
||||
// comes from the server identity, not this address). If a URL is later detected from a request, the next
|
||||
// touch overwrites the placeholder.
|
||||
serverAddress = Environment.MachineName;
|
||||
_logger.LogDebug(
|
||||
"No application URL available; registering server with placeholder address {ServerAddress}.",
|
||||
serverAddress);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug("Registering server with application URL {ServerAddress}.", serverAddress);
|
||||
}
|
||||
|
||||
try
|
||||
|
||||
@@ -384,57 +384,6 @@ using (ICoreScope scope = ScopeProvider.CreateCoreScope())
|
||||
3. **Lazy loading outside scope** - NPoco relationships must load within scope
|
||||
4. **Large migrations** - Split into multiple steps if > 1000 lines
|
||||
5. **Repository logic in services** - Keep repos thin, logic in services
|
||||
6. **Unbatched `WHERE IN` on user-sized collections** - See "Avoiding the SQL Server 2100-parameter limit" below
|
||||
|
||||
### Avoiding the SQL Server 2100-parameter limit
|
||||
|
||||
SQL Server caps a single statement at 2100 parameters. When an `IN` clause is built from a collection sized by user data, that cap can be hit — and the symptom is a runtime `SqlException` (error 8003) on customer installs that nobody hit in dev.
|
||||
|
||||
**The constant and helpers**:
|
||||
- `Constants.Sql.MaxParameterCount = 2000` (in `Umbraco.Core`, `Constants-Sql.cs`) — the ceiling we target (2100 minus headroom for joined predicates already in the SQL).
|
||||
- `IEnumerable<T>.InGroupsOf(groupSize)` (in `Umbraco.Core`, `Extensions/EnumerableExtensions.cs`) — extension method to batch a collection.
|
||||
- `Database.FetchByGroups<TResult, TSource>(source, groupSize, sqlFactory)` (in `Umbraco.Infrastructure`, `Persistence/NPocoDatabaseExtensions.cs`) — NPoco helper that batches a fetch.
|
||||
|
||||
**The safe patterns** (use one of these any time the collection size is user-driven):
|
||||
|
||||
```csharp
|
||||
// Pattern 1: batch a DeleteMany / Execute / Fetch by looping.
|
||||
foreach (IEnumerable<int> group in ids.InGroupsOf(Constants.Sql.MaxParameterCount))
|
||||
{
|
||||
Database.DeleteMany<FooDto>().Where(x => group.Contains(x.Id)).Execute();
|
||||
}
|
||||
|
||||
// Pattern 2: batched fetch with NPoco helper.
|
||||
List<FooDto> dtos = Database.FetchByGroups<FooDto, int>(
|
||||
ids,
|
||||
Constants.Sql.MaxParameterCount,
|
||||
batch => Sql().Select<FooDto>().From<FooDto>().WhereIn<FooDto>(x => x.Id, batch));
|
||||
|
||||
// Pattern 3: reserve headroom for other parameters in the same statement.
|
||||
foreach (IEnumerable<int> group in entityIds.InGroupsOf(Constants.Sql.MaxParameterCount - userGroupIds.Length))
|
||||
{
|
||||
// statement uses entityIds + userGroupIds, so subtract the other predicate's parameter count from the budget
|
||||
}
|
||||
```
|
||||
|
||||
**Decision rule when writing or reviewing a `WHERE IN`-style query**:
|
||||
|
||||
Look at what drives the size of the collection feeding the `IN`. Ask: *could this realistically exceed 2000 on a large install?* Risky drivers — batch any query backed by these:
|
||||
- All content / media / member nodes (or descendants of a deep tree).
|
||||
- A product of two scaling dimensions, e.g. `documents × languages`, `properties × versions`, `relations × endpoints`.
|
||||
- Configuration-tunable batch sizes (`CacheSettings.DocumentSeedBatchSize`, `NuCacheSettings.SqlPageSize`, etc.). The default may be safe but the customer can raise it.
|
||||
- Anything that scans property data, version history, relations, or audit logs across many nodes.
|
||||
|
||||
Safe drivers — don't bother batching:
|
||||
- Languages / content types / member groups / user groups — bounded by install configuration, typically <100.
|
||||
- "Per single content item" collections — properties on one document, versions of one document, tokens for one external login.
|
||||
- IDs supplied directly by a user action through the UI (picker selections, bulk actions on a page of results).
|
||||
|
||||
If you're not sure, batch — the cost is one loop and an `IEnumerable<T>` allocation per batch; the cost of being wrong is a SqlException on a customer's biggest site.
|
||||
|
||||
**For new public APIs** that take an `IEnumerable<int>`/`IEnumerable<Guid>` and feed it into a query, batch internally even if no current caller is large — package authors and future callers will not know about the 2000-limit ceiling.
|
||||
|
||||
**Don't** rely on `if (ids.Length > MaxParameterCount) throw` as a substitute for batching. Throwing only moves the problem; the caller has no obvious way to recover and will most likely just fail in production.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -104,7 +104,6 @@ internal sealed class JsonConfigManipulator : IConfigManipulator
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[Obsolete("This method is no longer used by Umbraco. Set the Umbraco:CMS:WebRouting:DisableRedirectUrlTracking configuration key instead. Scheduled for removal in Umbraco 19.")]
|
||||
public async Task SaveDisableRedirectUrlTrackingAsync(bool disable)
|
||||
=> await CreateOrUpdateConfigValueAsync(DisableRedirectUrlTrackingPath, disable);
|
||||
|
||||
|
||||
@@ -285,9 +285,7 @@ public static partial class UmbracoBuilderExtensions
|
||||
/// <returns>The same <see cref="Umbraco.Cms.Core.DependencyInjection.IUmbracoBuilder"/> instance so that multiple calls can be chained.</returns>
|
||||
public static IUmbracoBuilder AddPropertyIndexValueFactories(this IUmbracoBuilder builder)
|
||||
{
|
||||
builder.Services.AddSingleton<IBlockListPropertyIndexValueFactory, BlockListPropertyIndexValueFactory>();
|
||||
builder.Services.AddSingleton<IBlockGridPropertyIndexValueFactory, BlockGridPropertyIndexValueFactory>();
|
||||
builder.Services.AddSingleton<ISingleBlockPropertyIndexValueFactory, SingleBlockPropertyIndexValueFactory>();
|
||||
builder.Services.AddSingleton<IBlockValuePropertyIndexValueFactory, BlockValuePropertyIndexValueFactory>();
|
||||
builder.Services.AddSingleton<ITagPropertyIndexValueFactory, TagPropertyIndexValueFactory>();
|
||||
builder.Services.AddSingleton<IRichTextPropertyIndexValueFactory, RichTextPropertyIndexValueFactory>();
|
||||
builder.Services.AddSingleton<IDateOnlyPropertyIndexValueFactory, DateOnlyPropertyIndexValueFactory>();
|
||||
@@ -468,6 +466,7 @@ public static partial class UmbracoBuilderExtensions
|
||||
.AddNotificationHandler<MemberTypeChangedNotification, MemberTypeChangedDistributedCacheNotificationHandler>()
|
||||
.AddNotificationHandler<ContentTreeChangeNotification, ContentTreeChangeDistributedCacheNotificationHandler>()
|
||||
.AddNotificationHandler<ElementTreeChangeNotification, ElementTreeChangeDistributedCacheNotificationHandler>()
|
||||
.AddNotificationHandler<EntityContainerDeletedNotification, ElementContainerDeletedDistributedCacheNotificationHandler>()
|
||||
;
|
||||
|
||||
// add notification handlers for auditing
|
||||
|
||||
@@ -52,8 +52,8 @@ internal sealed class DatabaseDataCreator
|
||||
},
|
||||
new()
|
||||
{
|
||||
Name = "Find all logs that are within the namespace 'Umbraco.Cms'",
|
||||
Query = "StartsWith(SourceContext, 'Umbraco.Cms')",
|
||||
Name = "Find all logs that are from the namespace 'Umbraco.Core'",
|
||||
Query = "StartsWith(SourceContext, 'Umbraco.Core')",
|
||||
},
|
||||
new()
|
||||
{
|
||||
|
||||
@@ -76,7 +76,7 @@ public class MigrateSingleBlockList : AsyncMigrationBase
|
||||
SingleBlockListConfigurationCache blockListConfigurationCache,
|
||||
IDataValueEditorFactory dataValueEditorFactory,
|
||||
IIOHelper ioHelper,
|
||||
ISingleBlockPropertyIndexValueFactory blockValuePropertyIndexValueFactory,
|
||||
IBlockValuePropertyIndexValueFactory blockValuePropertyIndexValueFactory,
|
||||
IBlockEditorElementTypeCache elementTypeCache,
|
||||
AppCaches appCaches)
|
||||
: base(context)
|
||||
|
||||
@@ -7,7 +7,6 @@ namespace Umbraco.Cms.Infrastructure.Notifications
|
||||
/// <summary>
|
||||
/// Notification that is raised when a recurring background job is triggered or executed.
|
||||
/// </summary>
|
||||
// TODO (V19): Mark this class as abstract.
|
||||
public class RecurringBackgroundJobNotification : ObjectNotification<IRecurringBackgroundJob>
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
+24
@@ -1,6 +1,7 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Persistence.Repositories;
|
||||
using Umbraco.Cms.Infrastructure.Scoping;
|
||||
|
||||
@@ -34,4 +35,27 @@ internal sealed class ElementContainerRepository : EntityContainerRepository, IE
|
||||
cacheSyncService)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void PersistDeletedItem(EntityContainer entity)
|
||||
{
|
||||
if (entity == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(entity));
|
||||
}
|
||||
|
||||
// Element containers can be referenced as start nodes on individual users (umbracoUserStartNode)
|
||||
// and on user groups (umbracoUserGroup.startElementId). Both reference umbracoNode.id via FK,
|
||||
// so we must clear those references before deleting the underlying node.
|
||||
var args = new { id = entity.Id };
|
||||
Database.Execute(
|
||||
$"DELETE FROM {QuoteTableName(Constants.DatabaseSchema.Tables.UserStartNode)} WHERE {QuoteColumnName("startNode")} = @id",
|
||||
args);
|
||||
Database.Execute(
|
||||
$@"UPDATE {QuoteTableName(Constants.DatabaseSchema.Tables.UserGroup)}
|
||||
SET {QuoteColumnName("startElementId")} = NULL
|
||||
WHERE {QuoteColumnName("startElementId")} = @id",
|
||||
args);
|
||||
|
||||
base.PersistDeletedItem(entity);
|
||||
}
|
||||
}
|
||||
|
||||
+5
-15
@@ -249,24 +249,14 @@ internal sealed class RedirectUrlRepository : EntityRepositoryBase<Guid, IRedire
|
||||
|
||||
protected override IEnumerable<IRedirectUrl> PerformGetAll(params Guid[]? ids)
|
||||
{
|
||||
if (ids is null || ids.Length == 0)
|
||||
if (ids?.Length > Constants.Sql.MaxParameterCount)
|
||||
{
|
||||
return Database.Fetch<RedirectUrlDto>(GetBaseQuery(false))
|
||||
.WhereNotNull()
|
||||
.Select(Map)
|
||||
.WhereNotNull();
|
||||
}
|
||||
|
||||
// Batch the WhereIn fetch so we never exceed SQL Server's 2100 parameter limit.
|
||||
// EntityRepositoryBase.GetMany already groups IDs, but we keep the batching here as
|
||||
// a defensive measure for safety and consistency at the repository boundary.
|
||||
var dtos = new List<RedirectUrlDto>(ids.Length);
|
||||
foreach (IEnumerable<Guid> group in ids.InGroupsOf(Constants.Sql.MaxParameterCount))
|
||||
{
|
||||
Sql<ISqlContext> sql = GetBaseQuery(false).WhereIn<RedirectUrlDto>(x => x.Id, group);
|
||||
dtos.AddRange(Database.Fetch<RedirectUrlDto>(sql));
|
||||
throw new NotSupportedException(
|
||||
$"This repository does not support more than {Constants.Sql.MaxParameterCount} ids.");
|
||||
}
|
||||
|
||||
Sql<ISqlContext> sql = GetBaseQuery(false).WhereIn<RedirectUrlDto>(x => x.Id, ids);
|
||||
List<RedirectUrlDto> dtos = Database.Fetch<RedirectUrlDto>(sql);
|
||||
return dtos.WhereNotNull().Select(Map).WhereNotNull();
|
||||
}
|
||||
|
||||
|
||||
@@ -647,6 +647,8 @@ ON (tagset.tag = {cmsTags}.tag AND tagset.{group} = {cmsTags}.{group} AND COALES
|
||||
return Constants.ObjectTypes.Media;
|
||||
case TaggableObjectTypes.Member:
|
||||
return Constants.ObjectTypes.Member;
|
||||
case TaggableObjectTypes.Element:
|
||||
return Constants.ObjectTypes.Element;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(type));
|
||||
}
|
||||
|
||||
-1
@@ -75,7 +75,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
[
|
||||
sx.ColumnWithAlias("x", "otherId", "nodeId"),
|
||||
sx.ColumnWithAlias("n", "uniqueId", "nodeKey"),
|
||||
sx.ColumnWithAlias("n", "text", "nodeName"),
|
||||
sx.ColumnWithAlias("n", "nodeObjectType", "nodeObjectType"),
|
||||
$"COALESCE({sx.ColumnWithAlias("d", "published")}, {sx.ColumnWithAlias("e", "published")}) AS nodePublished",
|
||||
sx.ColumnWithAlias("ctn", "uniqueId", "contentTypeKey"),
|
||||
|
||||
+8
-42
@@ -110,10 +110,10 @@ public abstract class BlockEditorPropertyNotificationHandlerBase<TBlockLayoutIte
|
||||
|
||||
private void TraverseObject(JsonObject obj)
|
||||
{
|
||||
// we'll assume that the object is a data representation of a block based editor if it contains "contentData", "settingsData" and "layout".
|
||||
if (obj["contentData"] is JsonArray contentData && obj["settingsData"] is JsonArray settingsData && obj["layout"] is JsonObject layoutData)
|
||||
// we'll assume that the object is a data representation of a block based editor if it contains "contentData" and "settingsData".
|
||||
if (obj["contentData"] is JsonArray contentData && obj["settingsData"] is JsonArray settingsData)
|
||||
{
|
||||
ParseKeys(contentData, settingsData, layoutData);
|
||||
ParseKeys(contentData, settingsData);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -123,46 +123,12 @@ public abstract class BlockEditorPropertyNotificationHandlerBase<TBlockLayoutIte
|
||||
}
|
||||
}
|
||||
|
||||
private void ParseKeys(JsonArray contentData, JsonArray settingsData, JsonObject layoutData)
|
||||
private void ParseKeys(JsonArray contentData, JsonArray settingsData)
|
||||
{
|
||||
// recurse a JSON object to find all contained block editor layouts
|
||||
List<JsonObject> GetLayoutItemsRecursively(JsonObject jsonObject)
|
||||
{
|
||||
var layoutItems = new List<JsonObject>();
|
||||
if (jsonObject.ContainsKey("key") && jsonObject.ContainsKey("contentKey"))
|
||||
{
|
||||
// assume it's a layout if it has "key" and "contentKey"
|
||||
layoutItems.Add(jsonObject);
|
||||
}
|
||||
|
||||
foreach (JsonNode property in jsonObject.Select(v => v.Value).WhereNotNull())
|
||||
{
|
||||
IEnumerable<JsonObject> childrenToRecurse = property is JsonObject jsonObjectChild
|
||||
? [jsonObjectChild]
|
||||
: property is JsonArray jsonArrayChild
|
||||
? jsonArrayChild.OfType<JsonObject>()
|
||||
: [];
|
||||
layoutItems.AddRange(childrenToRecurse.SelectMany(GetLayoutItemsRecursively));
|
||||
}
|
||||
|
||||
return layoutItems;
|
||||
}
|
||||
|
||||
// grab keys applicable for replacement from all the layouts - that is:
|
||||
// - the key of the layout itself ("key").
|
||||
// - the key of the content item ("contentKey").
|
||||
// - ONLY for local content; do NOT replace content item keys for shared content.
|
||||
// - the key of the settings item ("settingsKey") if present.
|
||||
List<JsonObject> layoutItems = GetLayoutItemsRecursively(layoutData);
|
||||
var keys = layoutItems.SelectMany(layoutItem => new[]
|
||||
{
|
||||
layoutItem["key"]?.GetValue<string>(),
|
||||
layoutItem["isExternalContent"]?.GetValue<bool>() is not true
|
||||
? layoutItem["contentKey"]?.GetValue<string>()
|
||||
: null,
|
||||
layoutItem["settingsKey"]?.GetValue<string>(),
|
||||
})
|
||||
.WhereNotNull()
|
||||
// grab all keys from the objects of contentData and settingsData
|
||||
var keys = contentData.Select(c => c?["key"])
|
||||
.Union(settingsData.Select(s => s?["key"]))
|
||||
.Select(keyToken => keyToken?.GetValue<string>().NullOrWhiteSpaceAsNull())
|
||||
.ToArray();
|
||||
|
||||
// the following is solely for avoiding functionality wise breakage. we should consider removing it eventually, but for the time being it's harmless.
|
||||
|
||||
@@ -127,7 +127,7 @@ public abstract class BlockEditorPropertyValueEditor<TValue, TLayout> : BlockVal
|
||||
}
|
||||
|
||||
private static bool IsBlockEditorDataEmpty([NotNullWhen(false)] BlockEditorData<TValue, TLayout>? editorData)
|
||||
=> editorData is null || editorData.BlockValue.Layout.Count == 0;
|
||||
=> editorData is null || editorData.BlockValue.ContentData.Count == 0;
|
||||
|
||||
// We don't throw on error here because we want to be able to parse what we can, even if some of the data is invalid. In cases where migrating
|
||||
// from nested content to blocks, we don't want to trigger a fatal error for retrieving references, as this isn't vital to the operation.
|
||||
|
||||
@@ -63,20 +63,13 @@ public class BlockEditorValues<TValue, TLayout>
|
||||
|
||||
private BlockEditorData<TValue, TLayout>? Clean(BlockEditorData<TValue, TLayout> blockEditorData)
|
||||
{
|
||||
if (blockEditorData.BlockValue.Layout.Count == 0)
|
||||
if (blockEditorData.BlockValue.ContentData.Count == 0)
|
||||
{
|
||||
// if there's no content ensure there's no settings too
|
||||
blockEditorData.BlockValue.SettingsData.Clear();
|
||||
return null;
|
||||
}
|
||||
|
||||
if (blockEditorData.BlockValue.ContentData.Count == 0
|
||||
&& blockEditorData.BlockValue.SettingsData.Count == 0)
|
||||
{
|
||||
// no local content or settings; the block editor must contain only global elements
|
||||
return blockEditorData;
|
||||
}
|
||||
|
||||
var contentTypePropertyTypes = new Dictionary<string, Dictionary<string, IPropertyType>>();
|
||||
|
||||
// filter out any content that isn't referenced in the layout references
|
||||
|
||||
@@ -26,7 +26,7 @@ public class BlockGridPropertyEditor : BlockGridPropertyEditorBase
|
||||
public BlockGridPropertyEditor(
|
||||
IDataValueEditorFactory dataValueEditorFactory,
|
||||
IIOHelper ioHelper,
|
||||
IBlockGridPropertyIndexValueFactory blockValuePropertyIndexValueFactory)
|
||||
IBlockValuePropertyIndexValueFactory blockValuePropertyIndexValueFactory)
|
||||
: base(dataValueEditorFactory, blockValuePropertyIndexValueFactory)
|
||||
=> _ioHelper = ioHelper;
|
||||
|
||||
|
||||
@@ -25,9 +25,9 @@ namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
/// </summary>
|
||||
public abstract class BlockGridPropertyEditorBase : DataEditor, IValueSchemaProvider
|
||||
{
|
||||
private readonly IBlockGridPropertyIndexValueFactory _blockValuePropertyIndexValueFactory;
|
||||
private readonly IBlockValuePropertyIndexValueFactory _blockValuePropertyIndexValueFactory;
|
||||
|
||||
protected BlockGridPropertyEditorBase(IDataValueEditorFactory dataValueEditorFactory, IBlockGridPropertyIndexValueFactory blockValuePropertyIndexValueFactory)
|
||||
protected BlockGridPropertyEditorBase(IDataValueEditorFactory dataValueEditorFactory, IBlockValuePropertyIndexValueFactory blockValuePropertyIndexValueFactory)
|
||||
: base(dataValueEditorFactory)
|
||||
{
|
||||
_blockValuePropertyIndexValueFactory = blockValuePropertyIndexValueFactory;
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Models.Blocks;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
|
||||
internal sealed class BlockGridPropertyIndexValueFactory
|
||||
: BlockValuePropertyIndexValueFactoryBase<BlockGridValue>, IBlockGridPropertyIndexValueFactory
|
||||
{
|
||||
public BlockGridPropertyIndexValueFactory(
|
||||
PropertyEditorCollection propertyEditorCollection,
|
||||
IElementService elementService,
|
||||
IJsonSerializer jsonSerializer,
|
||||
IOptionsMonitor<IndexingSettings> indexingSettings)
|
||||
: base(propertyEditorCollection, elementService, jsonSerializer, indexingSettings)
|
||||
{
|
||||
}
|
||||
|
||||
protected override IEnumerable<RawDataItem> GetDataItems(BlockGridValue input, bool published)
|
||||
=> GetDataItems(input.GetLayouts() ?? [], input.ContentData, input.Expose, published);
|
||||
}
|
||||
@@ -25,7 +25,7 @@ public class BlockListPropertyEditor : BlockListPropertyEditorBase
|
||||
public BlockListPropertyEditor(
|
||||
IDataValueEditorFactory dataValueEditorFactory,
|
||||
IIOHelper ioHelper,
|
||||
IBlockListPropertyIndexValueFactory blockValuePropertyIndexValueFactory,
|
||||
IBlockValuePropertyIndexValueFactory blockValuePropertyIndexValueFactory,
|
||||
IJsonSerializer jsonSerializer)
|
||||
: base(dataValueEditorFactory, blockValuePropertyIndexValueFactory, jsonSerializer)
|
||||
=> _ioHelper = ioHelper;
|
||||
|
||||
@@ -21,13 +21,13 @@ namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
/// </summary>
|
||||
public abstract class BlockListPropertyEditorBase : DataEditor, IValueSchemaProvider
|
||||
{
|
||||
private readonly IBlockListPropertyIndexValueFactory _blockValuePropertyIndexValueFactory;
|
||||
private readonly IBlockValuePropertyIndexValueFactory _blockValuePropertyIndexValueFactory;
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BlockListPropertyEditorBase"/> class.
|
||||
/// </summary>
|
||||
protected BlockListPropertyEditorBase(IDataValueEditorFactory dataValueEditorFactory, IBlockListPropertyIndexValueFactory blockValuePropertyIndexValueFactory, IJsonSerializer jsonSerializer)
|
||||
protected BlockListPropertyEditorBase(IDataValueEditorFactory dataValueEditorFactory, IBlockValuePropertyIndexValueFactory blockValuePropertyIndexValueFactory, IJsonSerializer jsonSerializer)
|
||||
: base(dataValueEditorFactory)
|
||||
{
|
||||
_blockValuePropertyIndexValueFactory = blockValuePropertyIndexValueFactory;
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Models.Blocks;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
|
||||
internal sealed class BlockListPropertyIndexValueFactory
|
||||
: BlockValuePropertyIndexValueFactoryBase<BlockListValue>, IBlockListPropertyIndexValueFactory
|
||||
{
|
||||
public BlockListPropertyIndexValueFactory(
|
||||
PropertyEditorCollection propertyEditorCollection,
|
||||
IElementService elementService,
|
||||
IJsonSerializer jsonSerializer,
|
||||
IOptionsMonitor<IndexingSettings> indexingSettings)
|
||||
: base(propertyEditorCollection, elementService, jsonSerializer, indexingSettings)
|
||||
{
|
||||
}
|
||||
|
||||
protected override IEnumerable<RawDataItem> GetDataItems(BlockListValue input, bool published)
|
||||
=> GetDataItems(input.GetLayouts() ?? [], input.ContentData, input.Expose, published);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Models.Blocks;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
|
||||
namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
|
||||
internal sealed class BlockValuePropertyIndexValueFactory :
|
||||
BlockValuePropertyIndexValueFactoryBase<BlockValuePropertyIndexValueFactory.IndexValueFactoryBlockValue>,
|
||||
IBlockValuePropertyIndexValueFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BlockValuePropertyIndexValueFactory"/> class.
|
||||
/// </summary>
|
||||
/// <param name="propertyEditorCollection">The <see cref="PropertyEditorCollection"/> containing available property editors.</param>
|
||||
/// <param name="jsonSerializer">The <see cref="IJsonSerializer"/> used for serializing and deserializing JSON values.</param>
|
||||
/// <param name="indexingSettings">The <see cref="IOptionsMonitor{IndexingSettings}"/> providing access to indexing configuration settings.</param>
|
||||
public BlockValuePropertyIndexValueFactory(
|
||||
PropertyEditorCollection propertyEditorCollection,
|
||||
IJsonSerializer jsonSerializer,
|
||||
IOptionsMonitor<IndexingSettings> indexingSettings)
|
||||
: base(propertyEditorCollection, jsonSerializer, indexingSettings)
|
||||
{
|
||||
}
|
||||
|
||||
protected override IEnumerable<RawDataItem> GetDataItems(IndexValueFactoryBlockValue input, bool published)
|
||||
=> GetDataItems(input.ContentData, input.Expose, published);
|
||||
|
||||
// we only care about the content data when extracting values for indexing - not the layouts nor the settings
|
||||
internal sealed class IndexValueFactoryBlockValue
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the list of content block item data.
|
||||
/// </summary>
|
||||
public List<BlockItemData> ContentData { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the collection of <see cref="BlockItemVariation"/> instances that should be exposed by the index value factory.
|
||||
/// </summary>
|
||||
public List<BlockItemVariation> Expose { get; set; } = new();
|
||||
}
|
||||
}
|
||||
+22
-63
@@ -4,7 +4,6 @@ using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Blocks;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Infrastructure.Examine;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
@@ -13,17 +12,14 @@ namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
internal abstract class BlockValuePropertyIndexValueFactoryBase<TSerialized> : JsonPropertyIndexValueFactoryBase<TSerialized>
|
||||
{
|
||||
private readonly PropertyEditorCollection _propertyEditorCollection;
|
||||
private readonly IElementService _elementService;
|
||||
|
||||
protected BlockValuePropertyIndexValueFactoryBase(
|
||||
PropertyEditorCollection propertyEditorCollection,
|
||||
IElementService elementService,
|
||||
IJsonSerializer jsonSerializer,
|
||||
IOptionsMonitor<IndexingSettings> indexingSettings)
|
||||
: base(jsonSerializer, indexingSettings)
|
||||
{
|
||||
_propertyEditorCollection = propertyEditorCollection;
|
||||
_elementService = elementService;
|
||||
}
|
||||
|
||||
protected override IEnumerable<IndexValue> Handle(
|
||||
@@ -110,74 +106,37 @@ internal abstract class BlockValuePropertyIndexValueFactoryBase<TSerialized> : J
|
||||
/// <summary>
|
||||
/// Unwraps block item data as data items.
|
||||
/// </summary>
|
||||
protected IEnumerable<RawDataItem> GetDataItems(IEnumerable<IBlockLayoutItem> layouts, IList<BlockItemData> contentData, IList<BlockItemVariation> expose, bool published)
|
||||
protected IEnumerable<RawDataItem> GetDataItems(IList<BlockItemData> contentData, IList<BlockItemVariation> expose, bool published)
|
||||
{
|
||||
List<RawDataItem> indexData;
|
||||
if (published is false)
|
||||
{
|
||||
indexData = contentData.Select(ToRawData).ToList();
|
||||
return contentData.Select(ToRawData);
|
||||
}
|
||||
else
|
||||
|
||||
var indexData = new List<RawDataItem>();
|
||||
foreach (BlockItemData blockItemData in contentData)
|
||||
{
|
||||
indexData = new();
|
||||
foreach (BlockItemData blockItemData in contentData)
|
||||
var exposedCultures = expose
|
||||
.Where(e => e.ContentKey == blockItemData.Key)
|
||||
.Select(e => e.Culture)
|
||||
.ToArray();
|
||||
|
||||
if (exposedCultures.Any() is false)
|
||||
{
|
||||
var exposedCultures = expose
|
||||
.Where(e => e.ContentKey == blockItemData.Key)
|
||||
.Select(e => e.Culture)
|
||||
.ToArray();
|
||||
|
||||
if (exposedCultures.Any() is false)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (exposedCultures.Contains(null)
|
||||
|| exposedCultures.ContainsAll(blockItemData.Values.Select(v => v.Culture)))
|
||||
{
|
||||
indexData.Add(ToRawData(blockItemData));
|
||||
continue;
|
||||
}
|
||||
|
||||
indexData.Add(
|
||||
ToRawData(
|
||||
blockItemData.ContentTypeKey,
|
||||
blockItemData.Values.Where(value => value.Culture is null || exposedCultures.Contains(value.Culture))));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
IBlockLayoutItem[] layoutsAsArray = layouts as IBlockLayoutItem[] ?? layouts.ToArray();
|
||||
if (exposedCultures.Contains(null)
|
||||
|| exposedCultures.ContainsAll(blockItemData.Values.Select(v => v.Culture)))
|
||||
{
|
||||
indexData.Add(ToRawData(blockItemData));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the shared element keys from all layouts.
|
||||
// NOTE: While the Grid areas are modeled to contain areas within areas, in reality it cannot be configured as
|
||||
// such, so this "top-level aggregation" of shared content keys works in effect.
|
||||
Guid[] sharedElementKeys = layoutsAsArray
|
||||
.Union(layoutsAsArray.SelectMany(l => l.GetContainedLayouts()))
|
||||
.Where(l => l.IsExternalContent)
|
||||
.Select(l => l.ContentKey)
|
||||
.ToArray();
|
||||
|
||||
if (sharedElementKeys.Length > 0)
|
||||
{
|
||||
IEnumerable<IElement> elements = _elementService.GetByIds(sharedElementKeys);
|
||||
indexData.AddRange(
|
||||
elements.Select(element => new RawDataItem
|
||||
{
|
||||
ContentTypeKey = element.ContentType.Key,
|
||||
Properties = element
|
||||
.Properties
|
||||
.SelectMany(property => property
|
||||
.Values
|
||||
.Select(value => new RawPropertyData
|
||||
{
|
||||
Alias = property.Alias,
|
||||
Culture = value.Culture,
|
||||
Value = published
|
||||
? value.PublishedValue
|
||||
: value.EditedValue,
|
||||
}))
|
||||
.ToArray(),
|
||||
}));
|
||||
indexData.Add(
|
||||
ToRawData(
|
||||
blockItemData.ContentTypeKey,
|
||||
blockItemData.Values.Where(value => value.Culture is null || exposedCultures.Contains(value.Culture))));
|
||||
}
|
||||
|
||||
return indexData;
|
||||
|
||||
@@ -287,29 +287,11 @@ public abstract class BlockValuePropertyValueEditorBase<TValue, TLayout> : DataV
|
||||
|
||||
protected void MapBlockValueToEditor(IProperty property, TValue blockValue, string? culture, string? segment)
|
||||
{
|
||||
EnsureLayoutItemKeys(blockValue);
|
||||
|
||||
MapBlockItemDataToEditor(property, blockValue.ContentData, culture, segment);
|
||||
MapBlockItemDataToEditor(property, blockValue.SettingsData, culture, segment);
|
||||
_blockEditorVarianceHandler.AlignExposeVariance(blockValue);
|
||||
}
|
||||
|
||||
// Ensures that all layout items have a key (for backwards data format compatibility).
|
||||
private static void EnsureLayoutItemKeys(TValue blockValue)
|
||||
{
|
||||
if (!blockValue.Layout.TryGetValue(blockValue.PropertyEditorAlias, out IEnumerable<IBlockLayoutItem>? layout))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// All layout items with an empty key will be assigned the content key of the layout item.
|
||||
// This ensures data consistency across multiple sessions.
|
||||
foreach (IBlockLayoutItem layoutItem in layout.Where(layoutItem => layoutItem.Key == Guid.Empty))
|
||||
{
|
||||
layoutItem.Key = layoutItem.ContentKey;
|
||||
}
|
||||
}
|
||||
|
||||
protected IEnumerable<Guid> ConfiguredElementTypeKeys(IBlockConfiguration configuration)
|
||||
{
|
||||
yield return configuration.ContentElementTypeKey;
|
||||
|
||||
@@ -3,7 +3,6 @@ using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Infrastructure.Examine;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
@@ -20,16 +19,14 @@ internal sealed class RichTextPropertyIndexValueFactory : BlockValuePropertyInde
|
||||
/// </summary>
|
||||
/// <param name="propertyEditorCollection">A collection containing all available property editors.</param>
|
||||
/// <param name="jsonSerializer">The serializer used for handling JSON data.</param>
|
||||
/// <param name="elementService">Service for accessing elements.</param>
|
||||
/// <param name="indexingSettings">The monitor providing current indexing settings.</param>
|
||||
/// <param name="logger">The logger used for logging diagnostic information.</param>
|
||||
public RichTextPropertyIndexValueFactory(
|
||||
PropertyEditorCollection propertyEditorCollection,
|
||||
IElementService elementService,
|
||||
IJsonSerializer jsonSerializer,
|
||||
IOptionsMonitor<IndexingSettings> indexingSettings,
|
||||
ILogger<RichTextPropertyIndexValueFactory> logger)
|
||||
: base(propertyEditorCollection, elementService, jsonSerializer, indexingSettings)
|
||||
: base(propertyEditorCollection, jsonSerializer, indexingSettings)
|
||||
{
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_logger = logger;
|
||||
@@ -159,7 +156,7 @@ internal sealed class RichTextPropertyIndexValueFactory : BlockValuePropertyInde
|
||||
}
|
||||
|
||||
protected override IEnumerable<RawDataItem> GetDataItems(RichTextEditorValue input, bool published)
|
||||
=> GetDataItems(input.Blocks?.GetLayouts() ?? [], input.Blocks?.ContentData ?? [], input.Blocks?.Expose ?? [], published);
|
||||
=> GetDataItems(input.Blocks?.ContentData ?? [], input.Blocks?.Expose ?? [], published);
|
||||
|
||||
/// <summary>
|
||||
/// Strips HTML tags from content, replacing them with spaces to preserve word boundaries for indexing.
|
||||
|
||||
@@ -27,7 +27,7 @@ public class SingleBlockPropertyEditor : DataEditor
|
||||
{
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly IIOHelper _ioHelper;
|
||||
private readonly ISingleBlockPropertyIndexValueFactory _blockValuePropertyIndexValueFactory;
|
||||
private readonly IBlockValuePropertyIndexValueFactory _blockValuePropertyIndexValueFactory;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SingleBlockPropertyEditor"/> class.
|
||||
@@ -40,7 +40,7 @@ public class SingleBlockPropertyEditor : DataEditor
|
||||
IDataValueEditorFactory dataValueEditorFactory,
|
||||
IJsonSerializer jsonSerializer,
|
||||
IIOHelper ioHelper,
|
||||
ISingleBlockPropertyIndexValueFactory blockValuePropertyIndexValueFactory)
|
||||
IBlockValuePropertyIndexValueFactory blockValuePropertyIndexValueFactory)
|
||||
: base(dataValueEditorFactory)
|
||||
{
|
||||
_jsonSerializer = jsonSerializer;
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Models.Blocks;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
|
||||
internal sealed class SingleBlockPropertyIndexValueFactory
|
||||
: BlockValuePropertyIndexValueFactoryBase<SingleBlockValue>, ISingleBlockPropertyIndexValueFactory
|
||||
{
|
||||
public SingleBlockPropertyIndexValueFactory(
|
||||
PropertyEditorCollection propertyEditorCollection,
|
||||
IElementService elementService,
|
||||
IJsonSerializer jsonSerializer,
|
||||
IOptionsMonitor<IndexingSettings> indexingSettings)
|
||||
: base(propertyEditorCollection, elementService, jsonSerializer, indexingSettings)
|
||||
{
|
||||
}
|
||||
|
||||
protected override IEnumerable<RawDataItem> GetDataItems(SingleBlockValue input, bool published)
|
||||
=> GetDataItems(input.GetLayouts() ?? [], input.ContentData, input.Expose, published);
|
||||
}
|
||||
@@ -94,7 +94,7 @@ public sealed class BlockEditorConverter
|
||||
Key = data.Key,
|
||||
};
|
||||
|
||||
return _blockElementService.BuildElementAsync(owner, alignedData, preview).GetAwaiter().GetResult();
|
||||
return _blockElementService.BuildElementAsync(alignedData, preview).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
+25
-64
@@ -9,7 +9,6 @@ using Umbraco.Cms.Core.Models.Blocks;
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PropertyEditors.DeliveryApi;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Extensions;
|
||||
@@ -31,60 +30,10 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters
|
||||
private readonly BlockEditorVarianceHandler _blockEditorVarianceHandler;
|
||||
private readonly ILanguageService _languageService;
|
||||
private readonly IPropertyRenderingContextAccessor _propertyRenderingContextAccessor;
|
||||
private readonly IElementCacheService _elementCacheService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BlockGridPropertyValueConverter"/> class.
|
||||
/// </summary>
|
||||
/// <param name="proflog">The logger used for profiling and diagnostics.</param>
|
||||
/// <param name="blockConverter">The converter responsible for handling block editor values.</param>
|
||||
/// <param name="jsonSerializer">The serializer used for JSON serialization and deserialization.</param>
|
||||
/// <param name="apiElementBuilder">The builder for creating API elements from block data.</param>
|
||||
/// <param name="constructorCache">The cache for block grid property value constructors.</param>
|
||||
/// <param name="variationContextAccessor">Provides access to the current variation context.</param>
|
||||
/// <param name="blockEditorVarianceHandler">Handles variance logic for block editors.</param>
|
||||
/// <param name="languageService">Service for accessing all languages.</param>
|
||||
/// <param name="propertyRenderingContextAccessor">Provides access to the current rendering context.</param>
|
||||
/// <param name="elementCacheService">The cache for elements.</param>
|
||||
public BlockGridPropertyValueConverter(
|
||||
IProfilingLogger proflog,
|
||||
BlockEditorConverter blockConverter,
|
||||
IJsonSerializer jsonSerializer,
|
||||
IApiElementBuilder apiElementBuilder,
|
||||
BlockGridPropertyValueConstructorCache constructorCache,
|
||||
IVariationContextAccessor variationContextAccessor,
|
||||
BlockEditorVarianceHandler blockEditorVarianceHandler,
|
||||
ILanguageService languageService,
|
||||
IPropertyRenderingContextAccessor propertyRenderingContextAccessor,
|
||||
IElementCacheService elementCacheService)
|
||||
{
|
||||
_proflog = proflog;
|
||||
_blockConverter = blockConverter;
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_apiElementBuilder = apiElementBuilder;
|
||||
_constructorCache = constructorCache;
|
||||
_variationContextAccessor = variationContextAccessor;
|
||||
_blockEditorVarianceHandler = blockEditorVarianceHandler;
|
||||
_languageService = languageService;
|
||||
_propertyRenderingContextAccessor = propertyRenderingContextAccessor;
|
||||
_elementCacheService = elementCacheService;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="BlockGridPropertyValueConverter(IProfilingLogger, BlockEditorConverter, IJsonSerializer, IApiElementBuilder, BlockGridPropertyValueConstructorCache, IVariationContextAccessor, BlockEditorVarianceHandler, ILanguageService, IPropertyRenderingContextAccessor)"/>
|
||||
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 20.")]
|
||||
public BlockGridPropertyValueConverter(
|
||||
IProfilingLogger proflog,
|
||||
BlockEditorConverter blockConverter,
|
||||
IJsonSerializer jsonSerializer,
|
||||
IApiElementBuilder apiElementBuilder,
|
||||
BlockGridPropertyValueConstructorCache constructorCache,
|
||||
IVariationContextAccessor variationContextAccessor,
|
||||
BlockEditorVarianceHandler blockEditorVarianceHandler)
|
||||
: this(proflog, blockConverter, jsonSerializer, apiElementBuilder, constructorCache, variationContextAccessor, blockEditorVarianceHandler, StaticServiceProvider.Instance.GetRequiredService<ILanguageService>(), StaticServiceProvider.Instance.GetRequiredService<IPropertyRenderingContextAccessor>())
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("Please use the non-obsolete constructor. Scheduled for removal in V20.")]
|
||||
public BlockGridPropertyValueConverter(
|
||||
IProfilingLogger proflog,
|
||||
BlockEditorConverter blockConverter,
|
||||
@@ -95,17 +44,29 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters
|
||||
BlockEditorVarianceHandler blockEditorVarianceHandler,
|
||||
ILanguageService languageService,
|
||||
IPropertyRenderingContextAccessor propertyRenderingContextAccessor)
|
||||
: this(
|
||||
proflog,
|
||||
blockConverter,
|
||||
jsonSerializer,
|
||||
apiElementBuilder,
|
||||
constructorCache,
|
||||
variationContextAccessor,
|
||||
blockEditorVarianceHandler,
|
||||
languageService,
|
||||
propertyRenderingContextAccessor,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IElementCacheService>())
|
||||
{
|
||||
_proflog = proflog;
|
||||
_blockConverter = blockConverter;
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_apiElementBuilder = apiElementBuilder;
|
||||
_constructorCache = constructorCache;
|
||||
_variationContextAccessor = variationContextAccessor;
|
||||
_blockEditorVarianceHandler = blockEditorVarianceHandler;
|
||||
_languageService = languageService;
|
||||
_propertyRenderingContextAccessor = propertyRenderingContextAccessor;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="BlockGridPropertyValueConverter(IProfilingLogger, BlockEditorConverter, IJsonSerializer, IApiElementBuilder, BlockGridPropertyValueConstructorCache, IVariationContextAccessor, BlockEditorVarianceHandler, ILanguageService, IPropertyRenderingContextAccessor)"/>
|
||||
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 19.")]
|
||||
public BlockGridPropertyValueConverter(
|
||||
IProfilingLogger proflog,
|
||||
BlockEditorConverter blockConverter,
|
||||
IJsonSerializer jsonSerializer,
|
||||
IApiElementBuilder apiElementBuilder,
|
||||
BlockGridPropertyValueConstructorCache constructorCache,
|
||||
IVariationContextAccessor variationContextAccessor,
|
||||
BlockEditorVarianceHandler blockEditorVarianceHandler)
|
||||
: this(proflog, blockConverter, jsonSerializer, apiElementBuilder, constructorCache, variationContextAccessor, blockEditorVarianceHandler, StaticServiceProvider.Instance.GetRequiredService<ILanguageService>(), StaticServiceProvider.Instance.GetRequiredService<IPropertyRenderingContextAccessor>())
|
||||
{
|
||||
}
|
||||
|
||||
@@ -119,7 +80,7 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters
|
||||
|
||||
/// <inheritdoc />
|
||||
public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType)
|
||||
=> PropertyCacheLevel.Elements;
|
||||
=> PropertyCacheLevel.Element;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview)
|
||||
@@ -194,7 +155,7 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters
|
||||
return null;
|
||||
}
|
||||
|
||||
var creator = new BlockGridPropertyValueCreator(_blockConverter, _variationContextAccessor, _propertyRenderingContextAccessor, _blockEditorVarianceHandler, _elementCacheService, _jsonSerializer, _constructorCache, _languageService);
|
||||
var creator = new BlockGridPropertyValueCreator(_blockConverter, _variationContextAccessor, _propertyRenderingContextAccessor, _blockEditorVarianceHandler, _jsonSerializer, _constructorCache, _languageService);
|
||||
return creator.CreateBlockModelAsync(owner, referenceCacheLevel, intermediateBlockModelValue, preview, configuration.Blocks, configuration.GridColumns).GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
|
||||
+1
-5
@@ -1,6 +1,5 @@
|
||||
using Umbraco.Cms.Core.Models.Blocks;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Extensions;
|
||||
@@ -17,22 +16,19 @@ internal sealed class BlockGridPropertyValueCreator : BlockPropertyValueCreatorB
|
||||
/// </summary>
|
||||
/// <param name="blockEditorConverter">The service used to convert block editor data into strongly typed objects.</param>
|
||||
/// <param name="variationContextAccessor">Provides access to the current variation context for content.</param>
|
||||
/// <param name="propertyRenderingContextAccessor">Provides access to the current rendering context.</param>
|
||||
/// <param name="blockEditorVarianceHandler">Handles variance logic for block editors, such as culture or segment variations.</param>
|
||||
/// <param name="jsonSerializer">The serializer used to handle JSON data for block grid properties.</param>
|
||||
/// <param name="constructorCache">A cache for constructors used when creating block grid property values, improving performance.</param>
|
||||
/// <param name="languageService">Service used to retrieve language information for fallback resolution.</param>
|
||||
/// <param name="elementCacheService">The cache for elements.</param>
|
||||
public BlockGridPropertyValueCreator(
|
||||
BlockEditorConverter blockEditorConverter,
|
||||
IVariationContextAccessor variationContextAccessor,
|
||||
IPropertyRenderingContextAccessor propertyRenderingContextAccessor,
|
||||
BlockEditorVarianceHandler blockEditorVarianceHandler,
|
||||
IElementCacheService elementCacheService,
|
||||
IJsonSerializer jsonSerializer,
|
||||
BlockGridPropertyValueConstructorCache constructorCache,
|
||||
ILanguageService languageService)
|
||||
: base(blockEditorConverter, variationContextAccessor, propertyRenderingContextAccessor, blockEditorVarianceHandler, languageService, elementCacheService)
|
||||
: base(blockEditorConverter, variationContextAccessor, propertyRenderingContextAccessor, blockEditorVarianceHandler, languageService)
|
||||
{
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_constructorCache = constructorCache;
|
||||
|
||||
+3
-35
@@ -10,7 +10,6 @@ using Umbraco.Cms.Core.Models.Blocks;
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PropertyEditors.DeliveryApi;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Infrastructure.Extensions;
|
||||
@@ -35,7 +34,6 @@ public class BlockListPropertyValueConverter : PropertyValueConverterBase, IDeli
|
||||
private readonly BlockEditorVarianceHandler _blockEditorVarianceHandler;
|
||||
private readonly ILanguageService _languageService;
|
||||
private readonly IPropertyRenderingContextAccessor _propertyRenderingContextAccessor;
|
||||
private readonly IElementCacheService _elementCacheService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BlockListPropertyValueConverter"/> class.
|
||||
@@ -50,7 +48,6 @@ public class BlockListPropertyValueConverter : PropertyValueConverterBase, IDeli
|
||||
/// <param name="blockEditorVarianceHandler">Handles variance for block editors.</param>
|
||||
/// <param name="languageService">Service used to retrieve language information for fallback resolution.</param>
|
||||
/// <param name="propertyRenderingContextAccessor">Accessor for the current property rendering context.</param>
|
||||
/// <param name="elementCacheService">The cache for elements.</param>
|
||||
public BlockListPropertyValueConverter(
|
||||
IProfilingLogger proflog,
|
||||
BlockEditorConverter blockConverter,
|
||||
@@ -61,8 +58,7 @@ public class BlockListPropertyValueConverter : PropertyValueConverterBase, IDeli
|
||||
IVariationContextAccessor variationContextAccessor,
|
||||
BlockEditorVarianceHandler blockEditorVarianceHandler,
|
||||
ILanguageService languageService,
|
||||
IPropertyRenderingContextAccessor propertyRenderingContextAccessor,
|
||||
IElementCacheService elementCacheService)
|
||||
IPropertyRenderingContextAccessor propertyRenderingContextAccessor)
|
||||
{
|
||||
_proflog = proflog;
|
||||
_blockConverter = blockConverter;
|
||||
@@ -74,7 +70,6 @@ public class BlockListPropertyValueConverter : PropertyValueConverterBase, IDeli
|
||||
_blockEditorVarianceHandler = blockEditorVarianceHandler;
|
||||
_languageService = languageService;
|
||||
_propertyRenderingContextAccessor = propertyRenderingContextAccessor;
|
||||
_elementCacheService = elementCacheService;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="BlockListPropertyValueConverter(IProfilingLogger, BlockEditorConverter, IContentTypeService, IApiElementBuilder, IJsonSerializer, BlockListPropertyValueConstructorCache, IVariationContextAccessor, BlockEditorVarianceHandler, ILanguageService, IPropertyRenderingContextAccessor)"/>
|
||||
@@ -92,33 +87,6 @@ public class BlockListPropertyValueConverter : PropertyValueConverterBase, IDeli
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("Please use the non-obsolete constructor. Scheduled for removal in V20.")]
|
||||
public BlockListPropertyValueConverter(
|
||||
IProfilingLogger proflog,
|
||||
BlockEditorConverter blockConverter,
|
||||
IContentTypeService contentTypeService,
|
||||
IApiElementBuilder apiElementBuilder,
|
||||
IJsonSerializer jsonSerializer,
|
||||
BlockListPropertyValueConstructorCache constructorCache,
|
||||
IVariationContextAccessor variationContextAccessor,
|
||||
BlockEditorVarianceHandler blockEditorVarianceHandler,
|
||||
ILanguageService languageService,
|
||||
IPropertyRenderingContextAccessor propertyRenderingContextAccessor)
|
||||
: this(
|
||||
proflog,
|
||||
blockConverter,
|
||||
contentTypeService,
|
||||
apiElementBuilder,
|
||||
jsonSerializer,
|
||||
constructorCache,
|
||||
variationContextAccessor,
|
||||
blockEditorVarianceHandler,
|
||||
languageService,
|
||||
propertyRenderingContextAccessor,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IElementCacheService>())
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool IsConverter(IPublishedPropertyType propertyType)
|
||||
=> propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.BlockList);
|
||||
@@ -160,7 +128,7 @@ public class BlockListPropertyValueConverter : PropertyValueConverterBase, IDeli
|
||||
|
||||
/// <inheritdoc />
|
||||
public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType)
|
||||
=> PropertyCacheLevel.Elements;
|
||||
=> PropertyCacheLevel.Element;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview)
|
||||
@@ -228,7 +196,7 @@ public class BlockListPropertyValueConverter : PropertyValueConverterBase, IDeli
|
||||
return null;
|
||||
}
|
||||
|
||||
var creator = new BlockListPropertyValueCreator(_blockConverter, _variationContextAccessor, _propertyRenderingContextAccessor, _blockEditorVarianceHandler, _elementCacheService, _jsonSerializer, _constructorCache, _languageService);
|
||||
var creator = new BlockListPropertyValueCreator(_blockConverter, _variationContextAccessor, _propertyRenderingContextAccessor, _blockEditorVarianceHandler, _jsonSerializer, _constructorCache, _languageService);
|
||||
return creator.CreateBlockModelAsync(owner, referenceCacheLevel, intermediateBlockModelValue, preview, configuration.Blocks).GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
|
||||
+1
-5
@@ -1,6 +1,5 @@
|
||||
using Umbraco.Cms.Core.Models.Blocks;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
@@ -16,22 +15,19 @@ internal sealed class BlockListPropertyValueCreator : BlockPropertyValueCreatorB
|
||||
/// </summary>
|
||||
/// <param name="blockEditorConverter">The service used to convert block editor data into strongly typed objects.</param>
|
||||
/// <param name="variationContextAccessor">Provides access to the current variation context, used for handling content variations.</param>
|
||||
/// <param name="propertyRenderingContextAccessor">Provides access to the current rendering context.</param>
|
||||
/// <param name="blockEditorVarianceHandler">Handles variance logic for block editors, determining how values vary by culture or segment.</param>
|
||||
/// <param name="jsonSerializer">The serializer used for serializing and deserializing JSON data related to block list properties.</param>
|
||||
/// <param name="constructorCache">A cache that stores constructors for block list property values to improve performance.</param>
|
||||
/// <param name="languageService">Service used to retrieve language information for fallback resolution.</param>
|
||||
/// <param name="elementCacheService">The cache for elements.</param>
|
||||
public BlockListPropertyValueCreator(
|
||||
BlockEditorConverter blockEditorConverter,
|
||||
IVariationContextAccessor variationContextAccessor,
|
||||
IPropertyRenderingContextAccessor propertyRenderingContextAccessor,
|
||||
BlockEditorVarianceHandler blockEditorVarianceHandler,
|
||||
IElementCacheService elementCacheService,
|
||||
IJsonSerializer jsonSerializer,
|
||||
BlockListPropertyValueConstructorCache constructorCache,
|
||||
ILanguageService languageService)
|
||||
: base(blockEditorConverter, variationContextAccessor, propertyRenderingContextAccessor, blockEditorVarianceHandler, languageService, elementCacheService)
|
||||
: base(blockEditorConverter, variationContextAccessor, propertyRenderingContextAccessor, blockEditorVarianceHandler, languageService)
|
||||
{
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_constructorCache = constructorCache;
|
||||
|
||||
+17
-39
@@ -6,7 +6,6 @@ using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Blocks;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters;
|
||||
@@ -22,7 +21,6 @@ internal abstract class BlockPropertyValueCreatorBase<TBlockModel, TBlockItemMod
|
||||
private readonly IPropertyRenderingContextAccessor _propertyRenderingContextAccessor;
|
||||
private readonly BlockEditorVarianceHandler _blockEditorVarianceHandler;
|
||||
private readonly ILanguageService _languageService;
|
||||
private readonly IElementCacheService _elementCacheService;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a specific data converter for the block property implementation.
|
||||
@@ -66,14 +64,13 @@ internal abstract class BlockPropertyValueCreatorBase<TBlockModel, TBlockItemMod
|
||||
/// <returns></returns>
|
||||
protected delegate TBlockItemModel? EnrichBlockItemModelFromConfiguration(TBlockItemModel item, TBlockLayoutItem layoutItem, TBlockConfiguration configuration, CreateBlockItemModelFromLayout blockItemModelCreator);
|
||||
|
||||
protected BlockPropertyValueCreatorBase(BlockEditorConverter blockEditorConverter, IVariationContextAccessor variationContextAccessor, IPropertyRenderingContextAccessor propertyRenderingContextAccessor, BlockEditorVarianceHandler blockEditorVarianceHandler, ILanguageService languageService, IElementCacheService elementCacheService)
|
||||
protected BlockPropertyValueCreatorBase(BlockEditorConverter blockEditorConverter, IVariationContextAccessor variationContextAccessor, IPropertyRenderingContextAccessor propertyRenderingContextAccessor, BlockEditorVarianceHandler blockEditorVarianceHandler, ILanguageService languageService)
|
||||
{
|
||||
BlockEditorConverter = blockEditorConverter;
|
||||
_variationContextAccessor = variationContextAccessor;
|
||||
_propertyRenderingContextAccessor = propertyRenderingContextAccessor;
|
||||
_blockEditorVarianceHandler = blockEditorVarianceHandler;
|
||||
_languageService = languageService;
|
||||
_elementCacheService = elementCacheService;
|
||||
}
|
||||
|
||||
protected BlockEditorConverter BlockEditorConverter { get; }
|
||||
@@ -124,14 +121,17 @@ internal abstract class BlockPropertyValueCreatorBase<TBlockModel, TBlockItemMod
|
||||
CreateBlockModelFromItems createModelFromItems,
|
||||
EnrichBlockItemModelFromConfiguration? enrichBlockItem = null)
|
||||
{
|
||||
if (converted.Layout is null || converted.Layout.Any() is false)
|
||||
if (converted.BlockValue.ContentData.Count == 0)
|
||||
{
|
||||
return createEmptyModel();
|
||||
}
|
||||
|
||||
TBlockConfiguration[] blockConfigurationsAsArray = blockConfigurations as TBlockConfiguration[] ?? blockConfigurations.ToArray();
|
||||
var blockConfigMap = blockConfigurationsAsArray.ToDictionary(bc => bc.ContentElementTypeKey);
|
||||
var blockContentDataMap = converted.BlockValue.ContentData.ToDictionary(b => b.Key);
|
||||
if (converted.Layout is null)
|
||||
{
|
||||
return createEmptyModel();
|
||||
}
|
||||
|
||||
var blockConfigMap = blockConfigurations.ToDictionary(bc => bc.ContentElementTypeKey);
|
||||
VariationContext variationContext = _variationContextAccessor.VariationContext ?? new VariationContext();
|
||||
var languagesByIsoCode = (await _languageService.GetAllAsync())
|
||||
.ToDictionary(l => l.IsoCode, StringComparer.OrdinalIgnoreCase);
|
||||
@@ -139,33 +139,15 @@ internal abstract class BlockPropertyValueCreatorBase<TBlockModel, TBlockItemMod
|
||||
|
||||
// Convert the content data
|
||||
var contentPublishedElements = new Dictionary<Guid, IPublishedElement>();
|
||||
|
||||
// Get all layouts.
|
||||
// NOTE: While the Grid areas are modeled to contain areas within areas, in reality it cannot be configured as
|
||||
// such, so this "top-level aggregation" of layouts works in effect.
|
||||
IBlockLayoutItem[] allLayouts = converted
|
||||
.Layout
|
||||
.SelectMany(layout => new[] { layout }.Union(layout.GetContainedLayouts()))
|
||||
.ToArray();
|
||||
foreach (var layout in allLayouts)
|
||||
foreach (BlockItemData data in converted.BlockValue.ContentData)
|
||||
{
|
||||
IPublishedElement? element = null;
|
||||
BlockItemData? data = null;
|
||||
if (layout.IsExternalContent)
|
||||
if (!blockConfigMap.ContainsKey(data.ContentTypeKey))
|
||||
{
|
||||
element = await _elementCacheService.GetByKeyAsync(layout.ContentKey, preview);
|
||||
|
||||
if (preview is false && element?.IsPublished(variationContext.Culture) is false)
|
||||
{
|
||||
element = null;
|
||||
}
|
||||
}
|
||||
else if (blockContentDataMap.TryGetValue(layout.ContentKey, out data))
|
||||
{
|
||||
element = BlockEditorConverter.ConvertToElement(owner, data, referenceCacheLevel, preview);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (element is null)
|
||||
IPublishedElement? element = BlockEditorConverter.ConvertToElement(owner, data, referenceCacheLevel, preview);
|
||||
if (element == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -180,19 +162,15 @@ internal abstract class BlockPropertyValueCreatorBase<TBlockModel, TBlockItemMod
|
||||
? variationContext.Segment.NullOrWhiteSpaceAsNull()
|
||||
: null;
|
||||
|
||||
string? resolvedCulture = null;
|
||||
if (layout.IsExternalContent is false)
|
||||
Fallback fallback = _propertyRenderingContextAccessor.PropertyRenderingContext?.Fallback ?? default;
|
||||
if (BlockExposeFallbackHelper.IsBlockExposed(expose, element.Key, expectedBlockVariationCulture, expectedBlockVariationSegment, fallback, languagesByIsoCode, defaultIsoCode, out var resolvedCulture) is false)
|
||||
{
|
||||
Fallback fallback = _propertyRenderingContextAccessor.PropertyRenderingContext?.Fallback ?? default;
|
||||
if (BlockExposeFallbackHelper.IsBlockExposed(expose, element.Key, expectedBlockVariationCulture, expectedBlockVariationSegment, fallback, languagesByIsoCode, defaultIsoCode, out resolvedCulture) is false)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the block was exposed via fallback to a different culture, recreate the element
|
||||
// with that culture's variation context so its property values come from the resolved culture.
|
||||
if (resolvedCulture is not null && resolvedCulture.InvariantEquals(expectedBlockVariationCulture) is false && data is not null)
|
||||
if (resolvedCulture is not null && resolvedCulture.InvariantEquals(expectedBlockVariationCulture) is false)
|
||||
{
|
||||
VariationContext? originalContext = _variationContextAccessor.VariationContext;
|
||||
try
|
||||
|
||||
+1
-5
@@ -3,7 +3,6 @@
|
||||
|
||||
using Umbraco.Cms.Core.Models.Blocks;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
@@ -19,9 +18,7 @@ internal sealed class RichTextBlockPropertyValueCreator : BlockPropertyValueCrea
|
||||
/// </summary>
|
||||
/// <param name="blockEditorConverter">The <see cref="BlockEditorConverter"/> used to convert block editor values.</param>
|
||||
/// <param name="variationContextAccessor">The <see cref="IVariationContextAccessor"/> providing access to the variation context.</param>
|
||||
/// <param name="propertyRenderingContextAccessor">Provides access to the current rendering context.</param>
|
||||
/// <param name="blockEditorVarianceHandler">The <see cref="BlockEditorVarianceHandler"/> that handles block editor variance.</param>
|
||||
/// <param name="elementCacheService">The cache for elements.</param>
|
||||
/// <param name="jsonSerializer">The <see cref="IJsonSerializer"/> used for JSON serialization and deserialization.</param>
|
||||
/// <param name="constructorCache">The <see cref="RichTextBlockPropertyValueConstructorCache"/> used to cache rich text block property value constructors.</param>
|
||||
/// <param name="languageService">Service used to retrieve language information for fallback resolution.</param>
|
||||
@@ -30,11 +27,10 @@ internal sealed class RichTextBlockPropertyValueCreator : BlockPropertyValueCrea
|
||||
IVariationContextAccessor variationContextAccessor,
|
||||
IPropertyRenderingContextAccessor propertyRenderingContextAccessor,
|
||||
BlockEditorVarianceHandler blockEditorVarianceHandler,
|
||||
IElementCacheService elementCacheService,
|
||||
IJsonSerializer jsonSerializer,
|
||||
RichTextBlockPropertyValueConstructorCache constructorCache,
|
||||
ILanguageService languageService)
|
||||
: base(blockEditorConverter, variationContextAccessor, propertyRenderingContextAccessor, blockEditorVarianceHandler, languageService, elementCacheService)
|
||||
: base(blockEditorConverter, variationContextAccessor, propertyRenderingContextAccessor, blockEditorVarianceHandler, languageService)
|
||||
{
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_constructorCache = constructorCache;
|
||||
|
||||
+2
-46
@@ -15,7 +15,6 @@ using Umbraco.Cms.Core.Models.Blocks;
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PropertyEditors.DeliveryApi;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Strings;
|
||||
@@ -47,7 +46,6 @@ public class RteBlockRenderingValueConverter : SimpleRichTextValueConverter, IDe
|
||||
private readonly BlockEditorVarianceHandler _blockEditorVarianceHandler;
|
||||
private readonly ILanguageService _languageService;
|
||||
private readonly IPropertyRenderingContextAccessor _propertyRenderingContextAccessor;
|
||||
private readonly IElementCacheService _elementCacheService;
|
||||
|
||||
private DeliveryApiSettings _deliveryApiSettings;
|
||||
private readonly IDisposable? _deliveryApiSettingsChangeSubscription;
|
||||
@@ -71,7 +69,6 @@ public class RteBlockRenderingValueConverter : SimpleRichTextValueConverter, IDe
|
||||
/// <param name="deliveryApiSettingsMonitor">Monitors settings for the Delivery API.</param>
|
||||
/// <param name="languageService">Service used to retrieve language information for fallback resolution.</param>
|
||||
/// <param name="propertyRenderingContextAccessor">Accessor for the current property rendering context.</param>
|
||||
/// <param name="elementCacheService">The cache for elements.</param>
|
||||
public RteBlockRenderingValueConverter(
|
||||
HtmlLocalLinkParser linkParser,
|
||||
HtmlUrlParser urlParser,
|
||||
@@ -88,8 +85,7 @@ public class RteBlockRenderingValueConverter : SimpleRichTextValueConverter, IDe
|
||||
BlockEditorVarianceHandler blockEditorVarianceHandler,
|
||||
IOptionsMonitor<DeliveryApiSettings> deliveryApiSettingsMonitor,
|
||||
ILanguageService languageService,
|
||||
IPropertyRenderingContextAccessor propertyRenderingContextAccessor,
|
||||
IElementCacheService elementCacheService)
|
||||
IPropertyRenderingContextAccessor propertyRenderingContextAccessor)
|
||||
{
|
||||
_linkParser = linkParser;
|
||||
_urlParser = urlParser;
|
||||
@@ -106,7 +102,6 @@ public class RteBlockRenderingValueConverter : SimpleRichTextValueConverter, IDe
|
||||
_blockEditorVarianceHandler = blockEditorVarianceHandler;
|
||||
_languageService = languageService;
|
||||
_propertyRenderingContextAccessor = propertyRenderingContextAccessor;
|
||||
_elementCacheService = elementCacheService;
|
||||
|
||||
_deliveryApiSettings = deliveryApiSettingsMonitor.CurrentValue;
|
||||
_deliveryApiSettingsChangeSubscription = deliveryApiSettingsMonitor.OnChange(settings => _deliveryApiSettings = settings);
|
||||
@@ -133,45 +128,6 @@ public class RteBlockRenderingValueConverter : SimpleRichTextValueConverter, IDe
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("Please use the non-obsolete constructor. Scheduled for removal in V20.")]
|
||||
public RteBlockRenderingValueConverter(
|
||||
HtmlLocalLinkParser linkParser,
|
||||
HtmlUrlParser urlParser,
|
||||
HtmlImageSourceParser imageSourceParser,
|
||||
IApiRichTextElementParser apiRichTextElementParser,
|
||||
IApiRichTextMarkupParser apiRichTextMarkupParser,
|
||||
IPartialViewBlockEngine partialViewBlockEngine,
|
||||
BlockEditorConverter blockEditorConverter,
|
||||
IJsonSerializer jsonSerializer,
|
||||
IApiElementBuilder apiElementBuilder,
|
||||
RichTextBlockPropertyValueConstructorCache constructorCache,
|
||||
ILogger<RteBlockRenderingValueConverter> logger,
|
||||
IVariationContextAccessor variationContextAccessor,
|
||||
BlockEditorVarianceHandler blockEditorVarianceHandler,
|
||||
IOptionsMonitor<DeliveryApiSettings> deliveryApiSettingsMonitor,
|
||||
ILanguageService languageService,
|
||||
IPropertyRenderingContextAccessor propertyRenderingContextAccessor)
|
||||
: this(
|
||||
linkParser,
|
||||
urlParser,
|
||||
imageSourceParser,
|
||||
apiRichTextElementParser,
|
||||
apiRichTextMarkupParser,
|
||||
partialViewBlockEngine,
|
||||
blockEditorConverter,
|
||||
jsonSerializer,
|
||||
apiElementBuilder,
|
||||
constructorCache,
|
||||
logger,
|
||||
variationContextAccessor,
|
||||
blockEditorVarianceHandler,
|
||||
deliveryApiSettingsMonitor,
|
||||
languageService,
|
||||
propertyRenderingContextAccessor,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IElementCacheService>())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the cache level for the property.
|
||||
/// </summary>
|
||||
@@ -372,7 +328,7 @@ public class RteBlockRenderingValueConverter : SimpleRichTextValueConverter, IDe
|
||||
return null;
|
||||
}
|
||||
|
||||
var creator = new RichTextBlockPropertyValueCreator(_blockEditorConverter, _variationContextAccessor, _propertyRenderingContextAccessor, _blockEditorVarianceHandler, _elementCacheService, _jsonSerializer, _constructorCache, _languageService);
|
||||
var creator = new RichTextBlockPropertyValueCreator(_blockEditorConverter, _variationContextAccessor, _propertyRenderingContextAccessor, _blockEditorVarianceHandler, _jsonSerializer, _constructorCache, _languageService);
|
||||
return creator.CreateBlockModelAsync(owner, referenceCacheLevel, blocks, preview, configuration.Blocks).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
|
||||
+3
-23
@@ -12,7 +12,6 @@ using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PropertyEditors;
|
||||
using Umbraco.Cms.Core.PropertyEditors.DeliveryApi;
|
||||
using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Infrastructure.Extensions;
|
||||
@@ -37,7 +36,6 @@ public class SingleBlockPropertyValueConverter : PropertyValueConverterBase, IDe
|
||||
private readonly BlockEditorVarianceHandler _blockEditorVarianceHandler;
|
||||
private readonly ILanguageService _languageService;
|
||||
private readonly IPropertyRenderingContextAccessor _propertyRenderingContextAccessor;
|
||||
private readonly IElementCacheService _elementCacheService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SingleBlockPropertyValueConverter"/> class.
|
||||
@@ -51,7 +49,6 @@ public class SingleBlockPropertyValueConverter : PropertyValueConverterBase, IDe
|
||||
/// <param name="blockEditorVarianceHandler">Handles variance logic for block editors.</param>
|
||||
/// <param name="languageService">Service used to retrieve language information for fallback resolution.</param>
|
||||
/// <param name="propertyRenderingContextAccessor">Accessor for the current property rendering context.</param>
|
||||
/// <param name="elementCacheService">The cache for elements.</param>
|
||||
public SingleBlockPropertyValueConverter(
|
||||
IProfilingLogger proflog,
|
||||
BlockEditorConverter blockConverter,
|
||||
@@ -61,8 +58,7 @@ public class SingleBlockPropertyValueConverter : PropertyValueConverterBase, IDe
|
||||
IVariationContextAccessor variationContextAccessor,
|
||||
BlockEditorVarianceHandler blockEditorVarianceHandler,
|
||||
ILanguageService languageService,
|
||||
IPropertyRenderingContextAccessor propertyRenderingContextAccessor,
|
||||
IElementCacheService elementCacheService)
|
||||
IPropertyRenderingContextAccessor propertyRenderingContextAccessor)
|
||||
{
|
||||
_proflog = proflog;
|
||||
_blockConverter = blockConverter;
|
||||
@@ -73,7 +69,6 @@ public class SingleBlockPropertyValueConverter : PropertyValueConverterBase, IDe
|
||||
_blockEditorVarianceHandler = blockEditorVarianceHandler;
|
||||
_languageService = languageService;
|
||||
_propertyRenderingContextAccessor = propertyRenderingContextAccessor;
|
||||
_elementCacheService = elementCacheService;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="SingleBlockPropertyValueConverter(IProfilingLogger, BlockEditorConverter, IApiElementBuilder, IJsonSerializer, BlockListPropertyValueConstructorCache, IVariationContextAccessor, BlockEditorVarianceHandler, ILanguageService, IPropertyRenderingContextAccessor)"/>
|
||||
@@ -90,21 +85,6 @@ public class SingleBlockPropertyValueConverter : PropertyValueConverterBase, IDe
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("Please use the non-obsolete constructor. Scheduled for removal in V20.")]
|
||||
public SingleBlockPropertyValueConverter(
|
||||
IProfilingLogger proflog,
|
||||
BlockEditorConverter blockConverter,
|
||||
IApiElementBuilder apiElementBuilder,
|
||||
IJsonSerializer jsonSerializer,
|
||||
BlockListPropertyValueConstructorCache constructorCache,
|
||||
IVariationContextAccessor variationContextAccessor,
|
||||
BlockEditorVarianceHandler blockEditorVarianceHandler,
|
||||
ILanguageService languageService,
|
||||
IPropertyRenderingContextAccessor propertyRenderingContextAccessor)
|
||||
: this(proflog, blockConverter, apiElementBuilder, jsonSerializer, constructorCache, variationContextAccessor, blockEditorVarianceHandler, languageService, propertyRenderingContextAccessor, StaticServiceProvider.Instance.GetRequiredService<IElementCacheService>())
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool IsConverter(IPublishedPropertyType propertyType)
|
||||
=> propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.SingleBlock);
|
||||
@@ -114,7 +94,7 @@ public class SingleBlockPropertyValueConverter : PropertyValueConverterBase, IDe
|
||||
|
||||
/// <inheritdoc />
|
||||
public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType)
|
||||
=> PropertyCacheLevel.Elements;
|
||||
=> PropertyCacheLevel.Element;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview)
|
||||
@@ -169,7 +149,7 @@ public class SingleBlockPropertyValueConverter : PropertyValueConverterBase, IDe
|
||||
}
|
||||
|
||||
|
||||
var creator = new SingleBlockPropertyValueCreator(_blockConverter, _variationContextAccessor, _propertyRenderingContextAccessor, _blockEditorVarianceHandler, _elementCacheService, _jsonSerializer, _constructorCache, _languageService);
|
||||
var creator = new SingleBlockPropertyValueCreator(_blockConverter, _variationContextAccessor, _propertyRenderingContextAccessor, _blockEditorVarianceHandler, _jsonSerializer, _constructorCache, _languageService);
|
||||
return creator.CreateBlockModelAsync(owner, referenceCacheLevel, intermediateBlockModelValue, preview, configuration.Blocks).GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
|
||||
+1
-5
@@ -1,6 +1,5 @@
|
||||
using Umbraco.Cms.Core.Models.Blocks;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
@@ -17,9 +16,7 @@ internal sealed class SingleBlockPropertyValueCreator : BlockPropertyValueCreato
|
||||
/// </summary>
|
||||
/// <param name="blockEditorConverter">The service used to convert block editor data.</param>
|
||||
/// <param name="variationContextAccessor">Provides access to the current variation context.</param>
|
||||
/// <param name="propertyRenderingContextAccessor">Provides access to the current rendering context.</param>
|
||||
/// <param name="blockEditorVarianceHandler">Handles variance logic for block editors.</param>
|
||||
/// <param name="elementCacheService">The cache for elements.</param>
|
||||
/// <param name="jsonSerializer">The serializer used for JSON serialization and deserialization.</param>
|
||||
/// <param name="constructorCache">A cache for constructors used in block list property value creation.</param>
|
||||
/// <param name="languageService">Service used to retrieve language information for fallback resolution.</param>
|
||||
@@ -28,11 +25,10 @@ internal sealed class SingleBlockPropertyValueCreator : BlockPropertyValueCreato
|
||||
IVariationContextAccessor variationContextAccessor,
|
||||
IPropertyRenderingContextAccessor propertyRenderingContextAccessor,
|
||||
BlockEditorVarianceHandler blockEditorVarianceHandler,
|
||||
IElementCacheService elementCacheService,
|
||||
IJsonSerializer jsonSerializer,
|
||||
BlockListPropertyValueConstructorCache constructorCache,
|
||||
ILanguageService languageService)
|
||||
: base(blockEditorConverter, variationContextAccessor, propertyRenderingContextAccessor, blockEditorVarianceHandler, languageService, elementCacheService)
|
||||
: base(blockEditorConverter, variationContextAccessor, propertyRenderingContextAccessor, blockEditorVarianceHandler, languageService)
|
||||
{
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_constructorCache = constructorCache;
|
||||
|
||||
@@ -122,30 +122,11 @@ internal sealed class IndexedEntitySearchService : IIndexedEntitySearchService
|
||||
.Where(key => key != Guid.Empty)
|
||||
.ToArray();
|
||||
|
||||
// EntityService.GetAll returns entities in database (not Lucene score) order, which
|
||||
// would discard the relevance ranking. Re-order to match the search result sequence.
|
||||
IEnumerable<IEntitySlim> orderedItems;
|
||||
if (keys.Length > 0)
|
||||
{
|
||||
var keyOrder = new Dictionary<Guid, int>(keys.Length);
|
||||
for (var i = 0; i < keys.Length; i++)
|
||||
{
|
||||
keyOrder.TryAdd(keys[i], i);
|
||||
}
|
||||
|
||||
orderedItems = _entityService
|
||||
.GetAll(objectType, keys)
|
||||
.OrderBy(entity => keyOrder.TryGetValue(entity.Key, out var index) ? index : int.MaxValue)
|
||||
.ToArray();
|
||||
}
|
||||
else
|
||||
{
|
||||
orderedItems = [];
|
||||
}
|
||||
|
||||
return Task.FromResult(new PagedModel<IEntitySlim>
|
||||
{
|
||||
Items = orderedItems,
|
||||
Items = keys.Any()
|
||||
? _entityService.GetAll(objectType, keys)
|
||||
: Enumerable.Empty<IEntitySlim>(),
|
||||
Total = totalFound
|
||||
});
|
||||
}
|
||||
|
||||
@@ -47,21 +47,30 @@ public class LogViewerRepository : LogViewerRepositoryBase
|
||||
|
||||
var filesForCurrentDay = Directory.GetFiles(_loggingConfiguration.LogDirectory, filesToFind);
|
||||
|
||||
// Foreach file we find - open it. Any failure reading a single file (open error,
|
||||
// unrecoverable parse error, etc.) should not prevent the remaining files for the
|
||||
// day or date range from being read.
|
||||
// Foreach file we find - open it
|
||||
foreach (var filePath in filesForCurrentDay)
|
||||
{
|
||||
try
|
||||
// Open log file & add contents to the log collection
|
||||
// Which we then use LINQ to page over
|
||||
using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
|
||||
{
|
||||
ReadLogFile(filePath, logFilter, logs);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
ex,
|
||||
"Skipped log file {FilePath} after a file-level error; the file may be inaccessible or unreadable.",
|
||||
filePath);
|
||||
using (var stream = new StreamReader(fs))
|
||||
{
|
||||
var reader = new LogEventReader(stream);
|
||||
while (TryRead(reader, out LogEvent? evt))
|
||||
{
|
||||
// We may get a null if log line is malformed
|
||||
if (evt == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (logFilter.TakeLogEvent(evt))
|
||||
{
|
||||
logs.Add(evt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -79,63 +88,6 @@ public class LogViewerRepository : LogViewerRepositoryBase
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
private void ReadLogFile(string filePath, ILogFilter logFilter, List<LogEvent> logs)
|
||||
{
|
||||
using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||
using var stream = new StreamReader(fs);
|
||||
var reader = new LogEventReader(stream);
|
||||
|
||||
var errorCount = 0;
|
||||
Exception? firstError = null;
|
||||
|
||||
while (true)
|
||||
{
|
||||
LogEvent? evt;
|
||||
try
|
||||
{
|
||||
if (!reader.TryRead(out evt))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (ex is Newtonsoft.Json.JsonException or InvalidDataException)
|
||||
{
|
||||
// Serilog.Formatting.Compact.Reader uses Newtonsoft.Json internally and surfaces
|
||||
// its exceptions (Umbraco's own serialization is on System.Text.Json, but that
|
||||
// doesn't apply here — we have to catch what the reader actually throws).
|
||||
// JsonException covers parse failures (e.g. an unterminated string in a truncated
|
||||
// entry); InvalidDataException covers structurally-valid JSON that isn't a valid
|
||||
// Serilog Compact event. Either way the offending line has been consumed from the
|
||||
// underlying StreamReader and the next TryRead call advances. Anything else
|
||||
// (IOException, decoder failures, etc.) is propagated to the file-level catch in
|
||||
// GetLogs so we don't risk a tight loop or silently swallow a more serious failure.
|
||||
errorCount++;
|
||||
firstError ??= ex;
|
||||
continue;
|
||||
}
|
||||
|
||||
// LogEventReader may return true with a null event for a benign skip.
|
||||
if (evt is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (logFilter.TakeLogEvent(evt))
|
||||
{
|
||||
logs.Add(evt);
|
||||
}
|
||||
}
|
||||
|
||||
if (errorCount > 0)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
firstError,
|
||||
"Encountered {ErrorCount} unreadable line(s) while reading log file {FilePath}. The file may contain partially-written or corrupt entries; affected lines were skipped.",
|
||||
errorCount,
|
||||
filePath);
|
||||
}
|
||||
}
|
||||
|
||||
private IReadOnlyDictionary<string, string?> MapLogMessageProperties(IReadOnlyDictionary<string, LogEventPropertyValue>? properties)
|
||||
{
|
||||
var result = new Dictionary<string, string?>();
|
||||
@@ -169,4 +121,21 @@ public class LogViewerRepository : LogViewerRepositoryBase
|
||||
}
|
||||
|
||||
private static string GetSearchPattern(DateTime day) => $"*{day:yyyyMMdd}*.json";
|
||||
|
||||
private bool TryRead(LogEventReader reader, out LogEvent? evt)
|
||||
{
|
||||
try
|
||||
{
|
||||
return reader.TryRead(out evt);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// As we are reading/streaming one line at a time in the JSON file
|
||||
// Thus we can not report the line number, as it will always be 1
|
||||
_logger.LogError(ex, "Unable to parse a line in the JSON log file");
|
||||
|
||||
evt = null;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,28 +317,21 @@ internal sealed class DatabaseCacheRepository : RepositoryBase, IDatabaseCacheRe
|
||||
/// <inheritdoc/>
|
||||
public async Task<IEnumerable<ContentCacheNode>> GetDocumentSourcesAsync(IEnumerable<Guid> keys, bool preview = false)
|
||||
{
|
||||
// Batch the WHERE IN to stay within SQL Server's parameter limit.
|
||||
// The configurable document seed batch size is applied upstream; this method only enforces MaxParameterCount.
|
||||
Guid[] keysArray = keys as Guid[] ?? keys.ToArray();
|
||||
var dtos = new List<ContentSourceDto>(keysArray.Length);
|
||||
foreach (IEnumerable<Guid> group in keysArray.InGroupsOf(Constants.Sql.MaxParameterCount))
|
||||
{
|
||||
Sql<ISqlContext>? sql = SqlContentSourcesSelect()
|
||||
.Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Document))
|
||||
.WhereIn<NodeDto>(x => x.UniqueId, group)
|
||||
.Append(SqlOrderByLevelIdSortOrder(SqlContext));
|
||||
Sql<ISqlContext>? sql = SqlContentSourcesSelect()
|
||||
.Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Document))
|
||||
.WhereIn<NodeDto>(x => x.UniqueId, keys)
|
||||
.Append(SqlOrderByLevelIdSortOrder(SqlContext));
|
||||
|
||||
dtos.AddRange(await Database.FetchAsync<ContentSourceDto>(sql));
|
||||
}
|
||||
List<ContentSourceDto> dtos = await Database.FetchAsync<ContentSourceDto>(sql);
|
||||
|
||||
var filtered = dtos
|
||||
dtos = dtos
|
||||
.Where(x => x is not null)
|
||||
.Where(x => preview || ((x.PubDataRaw is not null || x.PubData is not null) && (!x.Published || x.PubName is not null)))
|
||||
.ToList();
|
||||
|
||||
IContentCacheDataSerializer serializer =
|
||||
_contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Document);
|
||||
return filtered
|
||||
return dtos
|
||||
.Select(x => CreateContentNodeKit(x, serializer, preview))
|
||||
.OfType<ContentCacheNode>();
|
||||
}
|
||||
@@ -503,27 +496,20 @@ internal sealed class DatabaseCacheRepository : RepositoryBase, IDatabaseCacheRe
|
||||
/// <inheritdoc/>
|
||||
public async Task<IEnumerable<ContentCacheNode>> GetMediaSourcesAsync(IEnumerable<Guid> keys)
|
||||
{
|
||||
// Batch the WHERE IN by Constants.Sql.MaxParameterCount so callers configuring
|
||||
// CacheSettings.MediaSeedBatchSize above that limit do not hit SQL Server's 2100 parameter limit.
|
||||
Guid[] keysArray = keys as Guid[] ?? keys.ToArray();
|
||||
var dtos = new List<ContentSourceDto>(keysArray.Length);
|
||||
foreach (IEnumerable<Guid> group in keysArray.InGroupsOf(Constants.Sql.MaxParameterCount))
|
||||
{
|
||||
Sql<ISqlContext>? sql = SqlMediaSourcesSelect()
|
||||
.Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Media))
|
||||
.WhereIn<NodeDto>(x => x.UniqueId, group)
|
||||
.Append(SqlOrderByLevelIdSortOrder(SqlContext));
|
||||
Sql<ISqlContext>? sql = SqlMediaSourcesSelect()
|
||||
.Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Media))
|
||||
.WhereIn<NodeDto>(x => x.UniqueId, keys)
|
||||
.Append(SqlOrderByLevelIdSortOrder(SqlContext));
|
||||
|
||||
dtos.AddRange(await Database.FetchAsync<ContentSourceDto>(sql));
|
||||
}
|
||||
List<ContentSourceDto> dtos = await Database.FetchAsync<ContentSourceDto>(sql);
|
||||
|
||||
var filtered = dtos
|
||||
dtos = dtos
|
||||
.Where(x => x is not null)
|
||||
.ToList();
|
||||
|
||||
IContentCacheDataSerializer serializer =
|
||||
_contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Media);
|
||||
return filtered
|
||||
return dtos
|
||||
.Select(x => CreateMediaNodeKit(x, serializer));
|
||||
}
|
||||
|
||||
@@ -743,135 +729,107 @@ internal sealed class DatabaseCacheRepository : RepositoryBase, IDatabaseCacheRe
|
||||
/// </summary>
|
||||
private List<CacheRebuildPublishableContentDto> GetDocumentMetadataForNodes(List<int> nodeIds)
|
||||
{
|
||||
// Query content metadata with both edit and published version info.
|
||||
// Query content metadata with both edit and published version info
|
||||
// Uses nested join pattern to ensure we only get the published ContentVersion
|
||||
// (where a DocumentVersionDto with Published=true exists).
|
||||
// Batched on nodeIds so a NuCacheSettings.SqlPageSize larger than MaxParameterCount still works.
|
||||
var results = new List<CacheRebuildPublishableContentDto>(nodeIds.Count);
|
||||
foreach (IEnumerable<int> group in nodeIds.InGroupsOf(Constants.Sql.MaxParameterCount))
|
||||
{
|
||||
Sql<ISqlContext> sql = Sql()
|
||||
.Select<NodeDto>(
|
||||
x => x.NodeId,
|
||||
x => x.UniqueId,
|
||||
x => x.Text,
|
||||
x => x.Path,
|
||||
x => x.Level,
|
||||
x => x.ParentId,
|
||||
x => x.SortOrder,
|
||||
x => x.CreateDate,
|
||||
x => Alias(x.UserId, "CreatorId"))
|
||||
.AndSelect<ContentDto>(x => x.ContentTypeId)
|
||||
.AndSelect<DocumentDto>(x => x.Published)
|
||||
.AndSelect<ContentVersionDto>(
|
||||
x => Alias(x.Id, "EditVersionId"),
|
||||
x => Alias(x.Text, "EditName"),
|
||||
x => Alias(x.VersionDate, "EditVersionDate"),
|
||||
x => Alias(x.UserId, "EditWriterId"))
|
||||
.AndSelect<ContentVersionDto>(
|
||||
"pcv",
|
||||
x => Alias(x.Id, "PublishedVersionId"),
|
||||
x => Alias(x.Text, "PublishedName"),
|
||||
x => Alias(x.VersionDate, "PublishedVersionDate"),
|
||||
x => Alias(x.UserId, "PublishedWriterId"))
|
||||
.From<NodeDto>()
|
||||
.InnerJoin<ContentDto>().On<NodeDto, ContentDto>((n, c) => n.NodeId == c.NodeId)
|
||||
.InnerJoin<DocumentDto>().On<NodeDto, DocumentDto>((n, d) => n.NodeId == d.NodeId)
|
||||
.InnerJoin<ContentVersionDto>().On<NodeDto, ContentVersionDto>((n, cv) => n.NodeId == cv.NodeId && cv.Current)
|
||||
// (where a DocumentVersionDto with Published=true exists)
|
||||
Sql<ISqlContext> sql = Sql()
|
||||
.Select<NodeDto>(
|
||||
x => x.NodeId,
|
||||
x => x.UniqueId,
|
||||
x => x.Text,
|
||||
x => x.Path,
|
||||
x => x.Level,
|
||||
x => x.ParentId,
|
||||
x => x.SortOrder,
|
||||
x => x.CreateDate,
|
||||
x => Alias(x.UserId, "CreatorId"))
|
||||
.AndSelect<ContentDto>(x => x.ContentTypeId)
|
||||
.AndSelect<DocumentDto>(x => x.Published)
|
||||
.AndSelect<ContentVersionDto>(
|
||||
x => Alias(x.Id, "EditVersionId"),
|
||||
x => Alias(x.Text, "EditName"),
|
||||
x => Alias(x.VersionDate, "EditVersionDate"),
|
||||
x => Alias(x.UserId, "EditWriterId"))
|
||||
.AndSelect<ContentVersionDto>(
|
||||
"pcv",
|
||||
x => Alias(x.Id, "PublishedVersionId"),
|
||||
x => Alias(x.Text, "PublishedName"),
|
||||
x => Alias(x.VersionDate, "PublishedVersionDate"),
|
||||
x => Alias(x.UserId, "PublishedWriterId"))
|
||||
.From<NodeDto>()
|
||||
.InnerJoin<ContentDto>().On<NodeDto, ContentDto>((n, c) => n.NodeId == c.NodeId)
|
||||
.InnerJoin<DocumentDto>().On<NodeDto, DocumentDto>((n, d) => n.NodeId == d.NodeId)
|
||||
.InnerJoin<ContentVersionDto>().On<NodeDto, ContentVersionDto>((n, cv) => n.NodeId == cv.NodeId && cv.Current)
|
||||
|
||||
// Nested join: ContentVersionDto "pcv" INNER JOIN DocumentVersionDto "pdv" ON published=true.
|
||||
// This ensures pcv only includes rows where there's a published DocumentVersion.
|
||||
.LeftJoin<ContentVersionDto>(
|
||||
j => j.InnerJoin<DocumentVersionDto>("pdv")
|
||||
.On<ContentVersionDto, DocumentVersionDto>(
|
||||
(left, right) => left.Id == right.Id && right.Published == true, "pcv", "pdv"),
|
||||
"pcv")
|
||||
// Nested join: ContentVersionDto "pcv" INNER JOIN DocumentVersionDto "pdv" ON published=true
|
||||
// This ensures pcv only includes rows where there's a published DocumentVersion
|
||||
.LeftJoin<ContentVersionDto>(
|
||||
j => j.InnerJoin<DocumentVersionDto>("pdv")
|
||||
.On<ContentVersionDto, DocumentVersionDto>(
|
||||
(left, right) => left.Id == right.Id && right.Published == true, "pcv", "pdv"),
|
||||
"pcv")
|
||||
|
||||
.On<NodeDto, ContentVersionDto>((n, cv) => n.NodeId == cv.NodeId, aliasRight: "pcv")
|
||||
.WhereIn<NodeDto>(x => x.NodeId, group);
|
||||
.On<NodeDto, ContentVersionDto>((n, cv) => n.NodeId == cv.NodeId, aliasRight: "pcv")
|
||||
.WhereIn<NodeDto>(x => x.NodeId, nodeIds);
|
||||
|
||||
results.AddRange(Database.Fetch<CacheRebuildPublishableContentDto>(sql));
|
||||
}
|
||||
|
||||
return results;
|
||||
return Database.Fetch<CacheRebuildPublishableContentDto>(sql);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets property data for the specified node IDs using efficient JOIN on nodeId.
|
||||
/// This avoids the expensive WHERE IN on versionId that causes index scans.
|
||||
/// Batched on nodeIds so a NuCacheSettings.SqlPageSize larger than MaxParameterCount still works.
|
||||
/// </summary>
|
||||
private List<CacheRebuildPropertyDto> GetPropertyDataForNodes(List<int> nodeIds)
|
||||
{
|
||||
var results = new List<CacheRebuildPropertyDto>();
|
||||
foreach (IEnumerable<int> group in nodeIds.InGroupsOf(Constants.Sql.MaxParameterCount))
|
||||
{
|
||||
// JOIN through nodeId → versionId path for efficient query plan.
|
||||
Sql<ISqlContext> sql = Sql()
|
||||
.Select<PropertyDataDto>(
|
||||
x => x.VersionId,
|
||||
x => x.LanguageId,
|
||||
x => x.Segment,
|
||||
x => x.IntegerValue,
|
||||
x => x.DecimalValue,
|
||||
x => x.DateValue,
|
||||
x => x.VarcharValue,
|
||||
x => x.TextValue)
|
||||
.AndSelect<PropertyTypeDto>(x => Alias(x.Alias, "PropertyAlias"))
|
||||
.From<PropertyDataDto>()
|
||||
.InnerJoin<PropertyTypeDto>().On<PropertyDataDto, PropertyTypeDto>((pd, pt) => pd.PropertyTypeId == pt.Id)
|
||||
.InnerJoin<ContentVersionDto>().On<PropertyDataDto, ContentVersionDto>((pd, cv) => pd.VersionId == cv.Id)
|
||||
.WhereIn<ContentVersionDto>(x => x.NodeId, group);
|
||||
// JOIN through nodeId → versionId path for efficient query plan
|
||||
Sql<ISqlContext> sql = Sql()
|
||||
.Select<PropertyDataDto>(
|
||||
x => x.VersionId,
|
||||
x => x.LanguageId,
|
||||
x => x.Segment,
|
||||
x => x.IntegerValue,
|
||||
x => x.DecimalValue,
|
||||
x => x.DateValue,
|
||||
x => x.VarcharValue,
|
||||
x => x.TextValue)
|
||||
.AndSelect<PropertyTypeDto>(x => Alias(x.Alias, "PropertyAlias"))
|
||||
.From<PropertyDataDto>()
|
||||
.InnerJoin<PropertyTypeDto>().On<PropertyDataDto, PropertyTypeDto>((pd, pt) => pd.PropertyTypeId == pt.Id)
|
||||
.InnerJoin<ContentVersionDto>().On<PropertyDataDto, ContentVersionDto>((pd, cv) => pd.VersionId == cv.Id)
|
||||
.WhereIn<ContentVersionDto>(x => x.NodeId, nodeIds);
|
||||
|
||||
results.AddRange(Database.Fetch<CacheRebuildPropertyDto>(sql));
|
||||
}
|
||||
|
||||
return results;
|
||||
return Database.Fetch<CacheRebuildPropertyDto>(sql);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets culture variation data for the specified node IDs.
|
||||
/// Batched on nodeIds so a NuCacheSettings.SqlPageSize larger than MaxParameterCount still works.
|
||||
/// </summary>
|
||||
private List<CacheRebuildCultureDto> GetCultureDataForNodes(List<int> nodeIds)
|
||||
{
|
||||
var results = new List<CacheRebuildCultureDto>();
|
||||
foreach (IEnumerable<int> group in nodeIds.InGroupsOf(Constants.Sql.MaxParameterCount))
|
||||
{
|
||||
Sql<ISqlContext> sql = Sql()
|
||||
.Select<ContentVersionCultureVariationDto>(x => x.VersionId, x => x.Name, x => x.UpdateDate)
|
||||
.AndSelect<LanguageDto>(x => Alias(x.IsoCode, "IsoCode"))
|
||||
.From<ContentVersionCultureVariationDto>()
|
||||
.InnerJoin<LanguageDto>().On<ContentVersionCultureVariationDto, LanguageDto>((cv, l) => cv.LanguageId == l.Id)
|
||||
.InnerJoin<ContentVersionDto>().On<ContentVersionCultureVariationDto, ContentVersionDto>((ccv, cv) => ccv.VersionId == cv.Id)
|
||||
.WhereIn<ContentVersionDto>(x => x.NodeId, group);
|
||||
Sql<ISqlContext> sql = Sql()
|
||||
.Select<ContentVersionCultureVariationDto>(x => x.VersionId, x => x.Name, x => x.UpdateDate)
|
||||
.AndSelect<LanguageDto>(x => Alias(x.IsoCode, "IsoCode"))
|
||||
.From<ContentVersionCultureVariationDto>()
|
||||
.InnerJoin<LanguageDto>().On<ContentVersionCultureVariationDto, LanguageDto>((cv, l) => cv.LanguageId == l.Id)
|
||||
.InnerJoin<ContentVersionDto>().On<ContentVersionCultureVariationDto, ContentVersionDto>((ccv, cv) => ccv.VersionId == cv.Id)
|
||||
.WhereIn<ContentVersionDto>(x => x.NodeId, nodeIds);
|
||||
|
||||
results.AddRange(Database.Fetch<CacheRebuildCultureDto>(sql));
|
||||
}
|
||||
|
||||
return results;
|
||||
return Database.Fetch<CacheRebuildCultureDto>(sql);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets document culture variation data (edited status per culture) for the specified node IDs. Used for documents.
|
||||
/// Batched on nodeIds so a NuCacheSettings.SqlPageSize larger than MaxParameterCount still works.
|
||||
/// </summary>
|
||||
private List<CacheRebuildPublishableCultureDto> GetDocumentCultureDataForNodes(List<int> nodeIds)
|
||||
{
|
||||
var results = new List<CacheRebuildPublishableCultureDto>();
|
||||
foreach (IEnumerable<int> group in nodeIds.InGroupsOf(Constants.Sql.MaxParameterCount))
|
||||
{
|
||||
Sql<ISqlContext> sql = Sql()
|
||||
.Select<DocumentCultureVariationDto>(x => x.NodeId, x => x.Edited)
|
||||
.AndSelect<LanguageDto>(x => Alias(x.IsoCode, "IsoCode"))
|
||||
.From<DocumentCultureVariationDto>()
|
||||
.InnerJoin<LanguageDto>().On<DocumentCultureVariationDto, LanguageDto>((dcv, l) => dcv.LanguageId == l.Id)
|
||||
.WhereIn<DocumentCultureVariationDto>(x => x.NodeId, group);
|
||||
Sql<ISqlContext> sql = Sql()
|
||||
.Select<DocumentCultureVariationDto>(x => x.NodeId, x => x.Edited)
|
||||
.AndSelect<LanguageDto>(x => Alias(x.IsoCode, "IsoCode"))
|
||||
.From<DocumentCultureVariationDto>()
|
||||
.InnerJoin<LanguageDto>().On<DocumentCultureVariationDto, LanguageDto>((dcv, l) => dcv.LanguageId == l.Id)
|
||||
.WhereIn<DocumentCultureVariationDto>(x => x.NodeId, nodeIds);
|
||||
|
||||
results.AddRange(Database.Fetch<CacheRebuildPublishableCultureDto>(sql));
|
||||
}
|
||||
|
||||
return results;
|
||||
return Database.Fetch<CacheRebuildPublishableCultureDto>(sql);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1400,38 +1358,31 @@ internal sealed class DatabaseCacheRepository : RepositoryBase, IDatabaseCacheRe
|
||||
|
||||
/// <summary>
|
||||
/// Gets content metadata for the specified node IDs using efficient JOIN. Used for media and members.
|
||||
/// Batched on nodeIds so a NuCacheSettings.SqlPageSize larger than MaxParameterCount still works.
|
||||
/// </summary>
|
||||
private List<CacheRebuildContentDto> GetContentMetadataForNodes(List<int> nodeIds)
|
||||
{
|
||||
var results = new List<CacheRebuildContentDto>(nodeIds.Count);
|
||||
foreach (IEnumerable<int> group in nodeIds.InGroupsOf(Constants.Sql.MaxParameterCount))
|
||||
{
|
||||
Sql<ISqlContext> sql = Sql()
|
||||
.Select<NodeDto>(
|
||||
x => x.NodeId,
|
||||
x => x.UniqueId,
|
||||
x => x.Text,
|
||||
x => x.Path,
|
||||
x => x.Level,
|
||||
x => x.ParentId,
|
||||
x => x.SortOrder,
|
||||
x => x.CreateDate,
|
||||
x => Alias(x.UserId, "CreatorId"))
|
||||
.AndSelect<ContentDto>(x => x.ContentTypeId)
|
||||
.AndSelect<ContentVersionDto>(
|
||||
x => Alias(x.Id, "VersionId"),
|
||||
x => Alias(x.VersionDate, "VersionDate"),
|
||||
x => Alias(x.UserId, "WriterId"))
|
||||
.From<NodeDto>()
|
||||
.InnerJoin<ContentDto>().On<NodeDto, ContentDto>((n, c) => n.NodeId == c.NodeId)
|
||||
.InnerJoin<ContentVersionDto>().On<NodeDto, ContentVersionDto>((n, cv) => n.NodeId == cv.NodeId && cv.Current)
|
||||
.WhereIn<NodeDto>(x => x.NodeId, group);
|
||||
Sql<ISqlContext> sql = Sql()
|
||||
.Select<NodeDto>(
|
||||
x => x.NodeId,
|
||||
x => x.UniqueId,
|
||||
x => x.Text,
|
||||
x => x.Path,
|
||||
x => x.Level,
|
||||
x => x.ParentId,
|
||||
x => x.SortOrder,
|
||||
x => x.CreateDate,
|
||||
x => Alias(x.UserId, "CreatorId"))
|
||||
.AndSelect<ContentDto>(x => x.ContentTypeId)
|
||||
.AndSelect<ContentVersionDto>(
|
||||
x => Alias(x.Id, "VersionId"),
|
||||
x => Alias(x.VersionDate, "VersionDate"),
|
||||
x => Alias(x.UserId, "WriterId"))
|
||||
.From<NodeDto>()
|
||||
.InnerJoin<ContentDto>().On<NodeDto, ContentDto>((n, c) => n.NodeId == c.NodeId)
|
||||
.InnerJoin<ContentVersionDto>().On<NodeDto, ContentVersionDto>((n, cv) => n.NodeId == cv.NodeId && cv.Current)
|
||||
.WhereIn<NodeDto>(x => x.NodeId, nodeIds);
|
||||
|
||||
results.AddRange(Database.Fetch<CacheRebuildContentDto>(sql));
|
||||
}
|
||||
|
||||
return results;
|
||||
return Database.Fetch<CacheRebuildContentDto>(sql);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,84 +1,45 @@
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Blocks;
|
||||
using Umbraco.Cms.Core.Models.Blocks;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Infrastructure.HybridCache.Factories;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.HybridCache.Services;
|
||||
|
||||
/// <inheritdoc/>
|
||||
internal class BlockElementService : IBlockElementService
|
||||
{
|
||||
private readonly IPublishedContentTypeCache _publishedContentTypeCache;
|
||||
private readonly IPublishedContentFactory _publishedContentFactory;
|
||||
private readonly IPublishedModelFactory _publishedModelFactory;
|
||||
private readonly ILanguageService _languageService;
|
||||
|
||||
public BlockElementService(
|
||||
IPublishedContentTypeCache publishedContentTypeCache,
|
||||
IPublishedContentFactory publishedContentFactory,
|
||||
IPublishedModelFactory publishedModelFactory,
|
||||
ILanguageService languageService)
|
||||
IPublishedModelFactory publishedModelFactory)
|
||||
{
|
||||
_publishedContentTypeCache = publishedContentTypeCache;
|
||||
_publishedContentFactory = publishedContentFactory;
|
||||
_publishedModelFactory = publishedModelFactory;
|
||||
_languageService = languageService;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<IPublishedElement?> BuildElementAsync(IPublishedElement owner, BlockItemData blockItemData, bool? preview = null)
|
||||
public Task<IPublishedElement?> BuildElementAsync(BlockItemData blockItemData, bool? preview = null)
|
||||
{
|
||||
ILanguage[]? allLanguages = null;
|
||||
ILanguage? defaultLanguage = null;
|
||||
|
||||
// Only convert element types - content types will cause an exception when PublishedModelFactory creates the model
|
||||
IPublishedContentType? publishedContentType = _publishedContentTypeCache.Get(PublishedItemType.Element, blockItemData.ContentTypeKey);
|
||||
if (publishedContentType is null || publishedContentType.IsElement is false)
|
||||
{
|
||||
return null;
|
||||
return Task.FromResult<IPublishedElement?>(null);
|
||||
}
|
||||
|
||||
var propertyData = new Dictionary<string, PropertyData[]>();
|
||||
foreach (IGrouping<string, BlockPropertyValue> properties in blockItemData.Values.GroupBy(value => value.Alias))
|
||||
{
|
||||
IPublishedPropertyType? propertyType = publishedContentType.GetPropertyType(properties.Key);
|
||||
if (propertyType is null)
|
||||
propertyData[properties.Key] = properties.Select(property => new PropertyData
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (propertyType.VariesByCulture() && owner.ContentType.VariesByCulture() is false)
|
||||
{
|
||||
// Special case:
|
||||
// The element property type varies by culture, but the owner element (e.g. the page) content type does not
|
||||
// vary by culture. Since the created element is fully culture aware at render time, we need to replicate
|
||||
// property values across all available languages, to make them available for rendering.
|
||||
|
||||
allLanguages ??= (await _languageService.GetAllAsync()).ToArray();
|
||||
defaultLanguage ??= allLanguages.SingleOrDefault(l => l.IsDefault)
|
||||
?? throw new InvalidOperationException("Could not find the default language.");
|
||||
|
||||
BlockPropertyValue property = properties.FirstOrDefault(p => p.Culture.InvariantEquals(defaultLanguage.IsoCode))
|
||||
?? properties.First();
|
||||
propertyData[properties.Key] = allLanguages.Select(language => new PropertyData
|
||||
{
|
||||
Culture = language.IsoCode,
|
||||
Segment = property.Segment ?? string.Empty, // throws an error if there is no value
|
||||
Value = property.Value,
|
||||
}).ToArray();
|
||||
}
|
||||
else
|
||||
{
|
||||
propertyData[properties.Key] = properties.Select(property => new PropertyData
|
||||
{
|
||||
Culture = property.Culture ?? string.Empty, // throws an error if there is no value
|
||||
Segment = property.Segment ?? string.Empty, // throws an error if there is no value
|
||||
Value = property.Value,
|
||||
}).ToArray();
|
||||
}
|
||||
Culture = property.Culture ?? string.Empty, // throws an error if there is no value
|
||||
Segment = property.Segment ?? string.Empty, // throws an error if there is no value
|
||||
Value = property.Value,
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
var published = preview is not true;
|
||||
@@ -86,15 +47,9 @@ internal class BlockElementService : IBlockElementService
|
||||
|
||||
const string name = "n/a";
|
||||
|
||||
IEnumerable<string> cultures = publishedContentType.VariesByCulture()
|
||||
? propertyData
|
||||
.SelectMany(p => p.Value.Select(v => v.Culture))
|
||||
.Where(c => c.IsNullOrWhiteSpace() is false)
|
||||
.OfType<string>()
|
||||
.Distinct()
|
||||
: [];
|
||||
|
||||
var cultureInfos = cultures.ToDictionary(
|
||||
var cultureInfos = (publishedContentType.VariesByCulture()
|
||||
? blockItemData.Values.Select(value => value.Culture).WhereNotNull().Distinct()
|
||||
: []).ToDictionary(
|
||||
culture => culture,
|
||||
_ => new CultureVariation
|
||||
{
|
||||
@@ -128,6 +83,6 @@ internal class BlockElementService : IBlockElementService
|
||||
};
|
||||
|
||||
var result = _publishedContentFactory.ToIPublishedElement(contentCacheNode, draft);
|
||||
return result.CreateModel(_publishedModelFactory);
|
||||
return Task.FromResult(result.CreateModel(_publishedModelFactory));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Concurrent;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
@@ -9,23 +9,41 @@ using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.Changes;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.HybridCache.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Implements <see cref="IDomainCacheService" />, providing an in-memory cache of the configured <see cref="Domain" />s.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The cache is lazily populated from the database on first access and kept up to date in response to domain
|
||||
/// cache refresher notifications. It is registered as a singleton, so a single instance serves all requests.
|
||||
/// </remarks>
|
||||
public class DomainCacheService : IDomainCacheService
|
||||
{
|
||||
private readonly IDomainService _domainService;
|
||||
private readonly ICoreScopeProvider _coreScopeProvider;
|
||||
private readonly ConcurrentDictionary<int, Domain> _domains;
|
||||
private bool _initialized = false;
|
||||
private readonly Lock _initializationLock = new();
|
||||
|
||||
// Both fields are written under _initializationLock but read on the hot path (request routing) without
|
||||
// it. Marking them volatile makes those lock-free reads acquire-reads, so a reader is guaranteed to see
|
||||
// the fully populated dictionary and the completed-initialization flag together, never a stale or
|
||||
// half-published value. This is required for correctness on weak memory models such as ARM; on x86/x64
|
||||
// ordinary reads already have acquire semantics, but we cannot rely on that.
|
||||
private volatile ConcurrentDictionary<int, Domain> _domains = new();
|
||||
private volatile bool _initialized;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DomainCacheService" /> class.
|
||||
/// </summary>
|
||||
/// <param name="domainService">The service used to load domains from the database.</param>
|
||||
/// <param name="coreScopeProvider">The provider used to create scopes for database access.</param>
|
||||
public DomainCacheService(IDomainService domainService, ICoreScopeProvider coreScopeProvider)
|
||||
{
|
||||
_domainService = domainService;
|
||||
_coreScopeProvider = coreScopeProvider;
|
||||
_domains = new ConcurrentDictionary<int, Domain>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<Domain> GetAll(bool includeWildcards)
|
||||
{
|
||||
InitializeIfMissing();
|
||||
@@ -34,22 +52,38 @@ public class DomainCacheService : IDomainCacheService
|
||||
: _domains.Select(x => x.Value).OrderBy(x => x.SortOrder);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the domains on first access, ensuring the cache is populated before any caller reads from it.
|
||||
/// </summary>
|
||||
private void InitializeIfMissing()
|
||||
{
|
||||
// Lazy, on-demand initialization triggered by the first request to reach the cache.
|
||||
// The flag must only be set to true *after* the domains have been loaded and published.
|
||||
// Setting it beforehand creates a window where a concurrent caller observes _initialized == true,
|
||||
// skips loading, and reads an empty domain cache. On a multi-site setup that empties domain
|
||||
// resolution, causing every site to fall back to the first root node (see ContentFinderByUrlNew).
|
||||
// The double-checked lock ensures a single load while concurrent readers block until it completes.
|
||||
if (_initialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_initialized = true;
|
||||
LoadDomains();
|
||||
|
||||
lock (_initializationLock)
|
||||
{
|
||||
if (_initialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
LoadDomains();
|
||||
_initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<Domain> GetAssigned(int documentId, bool includeWildcards = false)
|
||||
{
|
||||
InitializeIfMissing();
|
||||
// probably this could be optimized with an index
|
||||
// but then we'd need a custom DomainStore of some sort
|
||||
IEnumerable<Domain> list = _domains.Values.Where(x => x.ContentId == documentId);
|
||||
if (includeWildcards == false)
|
||||
{
|
||||
@@ -66,6 +100,7 @@ public class DomainCacheService : IDomainCacheService
|
||||
return documentId > 0 && GetAssigned(documentId, includeWildcards).Any();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Refresh(DomainCacheRefresher.JsonPayload[] payloads)
|
||||
{
|
||||
foreach (DomainCacheRefresher.JsonPayload payload in payloads)
|
||||
@@ -102,20 +137,23 @@ public class DomainCacheService : IDomainCacheService
|
||||
continue; // anomaly
|
||||
}
|
||||
|
||||
var newDomain = new Domain(domain.Id, domain.DomainName, domain.RootContentId.Value, culture, domain.IsWildcard, domain.SortOrder);
|
||||
|
||||
// Feels wierd to use key and oldvalue, but we're using neither when updating.
|
||||
_domains.AddOrUpdate(
|
||||
domain.Id,
|
||||
new Domain(domain.Id, domain.DomainName, domain.RootContentId.Value, culture, domain.IsWildcard, domain.SortOrder),
|
||||
(key, oldValue) => newDomain);
|
||||
_domains[domain.Id] = new Domain(domain.Id, domain.DomainName, domain.RootContentId.Value, culture, domain.IsWildcard, domain.SortOrder);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the configured domains from the database into a fresh dictionary and atomically swaps it in
|
||||
/// as the current cache.
|
||||
/// </summary>
|
||||
private void LoadDomains()
|
||||
{
|
||||
// Build the replacement set in a local dictionary and publish it with a single write to the
|
||||
// (volatile) _domains field. A reader never observes a partially populated cache during a RefreshAll
|
||||
// rebuild, and the published set contains exactly the current domains (any removed since the last
|
||||
// load are absent).
|
||||
var newDomains = new ConcurrentDictionary<int, Domain>();
|
||||
using (ICoreScope scope = _coreScopeProvider.CreateCoreScope())
|
||||
{
|
||||
scope.ReadLock(Constants.Locks.Domains);
|
||||
@@ -124,11 +162,11 @@ public class DomainCacheService : IDomainCacheService
|
||||
.Where(x => x.RootContentId.HasValue && x.LanguageIsoCode.IsNullOrWhiteSpace() == false)
|
||||
.Select(x => new Domain(x.Id, x.DomainName, x.RootContentId!.Value, x.LanguageIsoCode!, x.IsWildcard, x.SortOrder)))
|
||||
{
|
||||
_domains.AddOrUpdate(domain.Id, domain, (key, oldValue) => domain);
|
||||
newDomains[domain.Id] = domain;
|
||||
}
|
||||
scope.Complete();
|
||||
}
|
||||
|
||||
|
||||
_domains = newDomains;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,8 +77,6 @@ public class UmbracoApplicationBuilder : IUmbracoApplicationBuilder, IUmbracoEnd
|
||||
// Only use backoffice rewrites if backoffice is enabled
|
||||
if (ApplicationServices.GetService<IBackOfficeEnabledMarker>() is not null)
|
||||
{
|
||||
// Must run before the rewriter so the cache-bust hash is still present on the request path.
|
||||
AppBuilder.UseUmbracoBackOfficeCacheHeaders();
|
||||
AppBuilder.UseUmbracoBackOfficeRewrites();
|
||||
}
|
||||
|
||||
|
||||
@@ -62,8 +62,7 @@ Umbraco.Web.Common/
|
||||
│ └── UmbracoPublishedContentCultureProvider.cs
|
||||
├── Middleware/
|
||||
│ ├── BootFailedMiddleware.cs # Startup failure handling (81 lines)
|
||||
│ ├── PreviewAuthenticationMiddleware.cs # Preview mode auth (84 lines)
|
||||
│ └── UmbracoBackOfficeCacheHeadersMiddleware.cs # Cache-Control on cache-busted backoffice asset path
|
||||
│ └── PreviewAuthenticationMiddleware.cs # Preview mode auth (84 lines)
|
||||
├── Routing/
|
||||
│ ├── IAreaRoutes.cs # Area routing interface
|
||||
│ ├── IRoutableDocumentFilter.cs # Content routing filter
|
||||
@@ -257,8 +256,6 @@ ASP.NET Core Identity sign-in manager for members.
|
||||
|
||||
### Middleware
|
||||
|
||||
**Convention**: middleware lives in `Middleware/` as a class implementing `IMiddleware`, registered as a singleton next to its dependencies' registration (generic middleware in `AddWebComponents`; feature-specific middleware where the feature's services are added, e.g. backoffice middleware in `AddBackOfficeCore`), and wired into the pipeline via `app.UseMiddleware<TMiddleware>()`. Companion `IApplicationBuilder` extension methods are thin one-line `UseMiddleware<T>()` wrappers — inline `builder.Use(async …)` lambdas bypass DI and are harder to test; `CspNonceExtensions` and `Web.UI/WebApplicationExtensions` are tiny pre-existing exceptions, not a precedent for new work.
|
||||
|
||||
**BootFailedMiddleware** (lines 17-81):
|
||||
- Intercepts requests when `RuntimeLevel == BootFailed`
|
||||
- Debug mode: Rethrows exception for stack trace
|
||||
@@ -269,11 +266,6 @@ ASP.NET Core Identity sign-in manager for members.
|
||||
- Skips client-side requests and backoffice paths
|
||||
- Uses `IPreviewService.TryGetPreviewClaimsIdentityAsync()`
|
||||
|
||||
**UmbracoBackOfficeCacheHeadersMiddleware**:
|
||||
- Sets `Cache-Control: public, max-age=31536000, immutable` on responses under the cache-busted backoffice asset prefix (`/umbraco/backoffice/<hash>/…`); `no-cache` in debug mode
|
||||
- Runs before `UseUmbracoBackOfficeRewrites` so the original (hash-bearing) path can be matched
|
||||
- Non-destructive: uses `Response.OnStarting` + `ContainsKey` guard so any consumer override wins
|
||||
|
||||
---
|
||||
|
||||
## 4. Routing
|
||||
|
||||
@@ -229,19 +229,6 @@ public static class ApplicationBuilderExtensions
|
||||
return app;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers <see cref="UmbracoBackOfficeCacheHeadersMiddleware"/> to set the default
|
||||
/// <c>Cache-Control</c> header on responses served from the cache-busted BackOffice assets path.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// See <see cref="UmbracoBackOfficeCacheHeadersMiddleware"/> for behaviour, debug-mode semantics,
|
||||
/// and the precedence rules for consumer overrides. Must be registered before
|
||||
/// <see cref="UseUmbracoBackOfficeRewrites"/> so that the original request path (still containing
|
||||
/// the cache-bust hash) can be matched.
|
||||
/// </remarks>
|
||||
public static IApplicationBuilder UseUmbracoBackOfficeCacheHeaders(this IApplicationBuilder builder)
|
||||
=> builder.UseMiddleware<UmbracoBackOfficeCacheHeadersMiddleware>();
|
||||
|
||||
/// <summary>
|
||||
/// Configure a virtual path with IApplicationBuilder.UseRewriter for BackOffice assets to allow cache-busting using the url
|
||||
/// /umbraco/backoffice/!cache-busting-id!/assets/index.js => /umbraco/backoffice/assets/index.js.
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using Umbraco.Cms.Web.Common.Hosting;
|
||||
using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment;
|
||||
|
||||
namespace Umbraco.Cms.Web.Common.Middleware;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the default <c>Cache-Control</c> response header on requests served from the cache-busted
|
||||
/// BackOffice assets path (<c>/umbraco/backoffice/<hash>/...</c>).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The path prefix contains a deployment-wide hash derived from the Umbraco version
|
||||
/// (see <see cref="IBackOfficePathGenerator.BackOfficeCacheBustHash"/>). Because the URL itself
|
||||
/// changes whenever the version changes, all responses served under that prefix are safe to mark
|
||||
/// as <c>immutable</c> with a long <c>max-age</c>, regardless of whether the on-disk filename
|
||||
/// contains a content hash.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// In debug mode the underlying built assets may change while the app is running (typically
|
||||
/// from a developer rebuilding the backoffice without restarting the host). The header is
|
||||
/// therefore set to <c>no-cache</c>, which still allows the browser to store the response
|
||||
/// but forces an <c>If-None-Match</c> revalidation on the next request — yielding fast 304s
|
||||
/// when nothing has changed and full 200s when the file on disk has been rebuilt.
|
||||
/// <c>no-store</c> would force a full re-download on every request, which is unnecessary.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This middleware is non-destructive to consumer customisation:
|
||||
/// <list type="bullet">
|
||||
/// <item>
|
||||
/// The header is only set when no <c>Cache-Control</c> value is already present on the
|
||||
/// response, so synchronous overrides written upstream (including
|
||||
/// <c>StaticFileOptions.OnPrepareResponse</c>) take precedence.
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// The header is set via <c>HttpResponse.OnStarting</c>; consumer callbacks registered
|
||||
/// later in the pipeline fire first (LIFO) and can therefore override the default.
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// Non-2xx responses (e.g. 404) are not marked as immutable to avoid long-lived caching
|
||||
/// of error responses.
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Must run before <see cref="Umbraco.Extensions.ApplicationBuilderExtensions.UseUmbracoBackOfficeRewrites"/>
|
||||
/// so the original request path (still containing the cache-bust hash) can be matched.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <seealso cref="Microsoft.AspNetCore.Http.IMiddleware" />
|
||||
public class UmbracoBackOfficeCacheHeadersMiddleware : IMiddleware
|
||||
{
|
||||
private readonly string _prefix;
|
||||
private readonly string _headerValue;
|
||||
|
||||
public UmbracoBackOfficeCacheHeadersMiddleware(
|
||||
IBackOfficePathGenerator backOfficePathGenerator,
|
||||
IHostingEnvironment hostingEnvironment)
|
||||
{
|
||||
// Normalise to a single leading slash, no trailing slash — defensive against any
|
||||
// future change in IBackOfficePathGenerator's output shape.
|
||||
_prefix = "/" + backOfficePathGenerator.BackOfficeAssetsPath.TrimStart('/').TrimEnd('/');
|
||||
_headerValue = hostingEnvironment.IsDebugMode
|
||||
? "no-cache"
|
||||
: "public, max-age=31536000, immutable";
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
|
||||
{
|
||||
if (IsCacheableAssetRequest(context.Request))
|
||||
{
|
||||
context.Response.OnStarting(static state =>
|
||||
{
|
||||
(HttpResponse response, string value) = ((HttpResponse, string))state;
|
||||
if (ShouldSetCacheControl(response))
|
||||
{
|
||||
response.Headers[HeaderNames.CacheControl] = value;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}, (context.Response, _headerValue));
|
||||
}
|
||||
|
||||
await next(context);
|
||||
}
|
||||
|
||||
// Only GET/HEAD: POST/PUT/DELETE responses aren't cacheable in the immutable sense and
|
||||
// OPTIONS is used for CORS preflight, where a long cache lifetime would prevent the
|
||||
// browser from re-issuing preflights when needed.
|
||||
private bool IsCacheableAssetRequest(HttpRequest request)
|
||||
=> (HttpMethods.IsGet(request.Method) || HttpMethods.IsHead(request.Method))
|
||||
&& request.Path.StartsWithSegments(_prefix, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
// Include 304 alongside 2xx: intermediate caches (CDNs, proxies) use the Cache-Control on
|
||||
// the 304 response to update freshness for the cached body.
|
||||
private static bool ShouldSetCacheControl(HttpResponse response)
|
||||
=> response.StatusCode is (>= 200 and < 300) or 304
|
||||
&& !response.Headers.ContainsKey(HeaderNames.CacheControl);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user