Skip to content

Merge handler settings over HandlerDefaults instead of replacing them - #1001

Merged
KevinJump merged 3 commits into
v18/mainfrom
v18/handler-settings-merge
Jul 23, 2026
Merged

Merge handler settings over HandlerDefaults instead of replacing them#1001
KevinJump merged 3 commits into
v18/mainfrom
v18/handler-settings-merge

Conversation

@KevinJump

Copy link
Copy Markdown
Owner

What

When a handler defines its own settings block, uSync previously ignored the set's HandlerDefaults entirelyGetHandlerSettings returned the handler's Clone(), so anything set only in HandlerDefaults did not apply to that handler. This is the "phantom pending change" footgun behind e.g. #953-style reports, where a per-handler CreateOnly:"true" works but relying on defaults for other settings silently doesn't.

This PR makes a handler's own block layer over the defaults, so a handler only needs to specify what it wants to change.

How

  • MergeWithDefaults (uSyncHandlerSettings.cs) — merges the Settings dictionary key-by-key (defaults as base, handler keys win). Used lazily by GetHandlerSettings, so keys added to HandlerDefaults.Settings by later post-configure steps still cascade (idempotent).
  • ConfigureHandlerSet (HandlerSetConfigurationExtensions.cs) — options-layer helper that binds each handler's raw config on top of a clone of HandlerDefaults. This is what makes the strongly typed properties (UseFlatStructure, GuidNames, …) cascade — an unset bool can't be told apart from its default value once bound, so it has to happen at bind time. The Default set is wired through it; the helper is reusable so Complete's named sets can adopt it.
  • Clone() fix — it silently dropped CreateClean and FullFileOnDifference; both are now preserved. (Latent bug: handlers setting either in their own block were always resolved as false.)

Why hybrid (lazy dict merge + eager typed merge): Complete's named sets bind from arbitrary config sections and their post-configures mutate HandlerDefaults after binding, so a single eager merge can't see late changes and a generic name→section approach is unsafe.

Breaking change

A handler block that defined its own settings used to replace the defaults; it now inherits unset defaults. Configs that relied on the replacement behaviour will see inherited values instead. Documented in CHANGELOG.md (new file).

Tests

11 new tests under uSync.Tests/Configuration/ covering the dictionary merge, strongly-typed cascade, minimal/empty-block handlers, no-mutation of inputs, and the Clone() regression. Full suite: 148 passing.

Follow-up (not in this PR)

Switch Complete's Configure<uSyncHandlerSetSettings>(name, section) calls (Publisher, Snapshots, Exporter, Restore) to ConfigureHandlerSet(name, section) so its sets get the same cascade — mind ordering with its HandlerDefaults-mutating post-configures.

🤖 Generated with Claude Code

KevinJump and others added 3 commits July 23, 2026 11:54
A handler that defined its own settings block previously ignored the set's
HandlerDefaults entirely (GetHandlerSettings returned the handler's Clone()).
This meant per-key defaults such as CreateOnly, and strongly typed defaults
such as UseFlatStructure, silently did not apply to any handler with its own
block - a common "phantom pending change" footgun.

Changes:
- MergeWithDefaults: merges the Settings dictionary (defaults as base, handler
  keys win). Used lazily by GetHandlerSettings so late additions to
  HandlerDefaults.Settings still cascade.
- ConfigureHandlerSet: options-layer helper that binds each handler's raw
  config over a clone of HandlerDefaults, so the strongly typed properties
  cascade too (an unset bool can't be distinguished from its default once
  bound, so this has to happen at bind time). Reusable so Complete's named
  sets can adopt it.
- Clone() now preserves CreateClean and FullFileOnDifference (latent bug).

Behavioural/breaking change - documented in CHANGELOG.md. Targeting v18.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add historical sections for 18.0.0, 18.0.2 and 18.0.3 based on the release
build commits on v18/main (the v18.0.0 / v18.0.8 tags are degenerate - both
point at the same commit, so point releases aren't individually tagged).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@KevinJump
KevinJump merged commit c42f415 into v18/main Jul 23, 2026
5 checks passed
@KevinJump
KevinJump deleted the v18/handler-settings-merge branch July 23, 2026 11:05
glitchedmob added a commit to sgfdevs/cms.methodconf.com that referenced this pull request Aug 23, 2026
Updated
[Microsoft.EntityFrameworkCore.Design](https://github.com/dotnet/dotnet)
from 10.0.10 to 10.0.11.

<details>
<summary>Release notes</summary>

_Sourced from [Microsoft.EntityFrameworkCore.Design's
releases](https://github.com/dotnet/dotnet/releases)._

No release notes found for this version range.

Commits viewable in [compare
view](https://github.com/dotnet/dotnet/commits).
</details>

Updated [Microsoft.NET.Test.Sdk](https://github.com/microsoft/vstest)
from 18.8.1 to 18.9.0.

<details>
<summary>Release notes</summary>

_Sourced from [Microsoft.NET.Test.Sdk's
releases](https://github.com/microsoft/vstest/releases)._

## 18.9.0

## What's Changed
* Fix tilde/exclamation characters corrupted in TerminalLogger test
output by @​nohwnd in microsoft/vstest#16046
* Make TranslationLayer Native AOT-compatible by @​drewnoakes in
microsoft/vstest#16045
* Guard GenerateProgramFile target against UseWinUI/UseUwpTools
evaluation order by @​nohwnd in
microsoft/vstest#16072
* Add RequestingAssembly to AssemblyResolveEventArgs for binary compat
by @​nohwnd in microsoft/vstest#16076
* Remove stale Microsoft.Extensions.FileSystemGlobbing binding redirect
from testhost.x86 and datacollector by @​Evangelink in
microsoft/vstest#16082
* Fix TRX attachment paths when LogFileName contains a subdirectory by
@​nohwnd in microsoft/vstest#15791
* Fix missing dumps for .NET Framework child processes in
NetClientHangDumper by @​nohwnd in
microsoft/vstest#16098
* Fix data collection channels to use negotiated protocol version
instead of V1 by @​nohwnd in
microsoft/vstest#16096
* Fix race condition in BlameCollector: skip hang dump when testhost
hasn't launched yet by @​nohwnd in
microsoft/vstest#16065
* Replace TestSDKAutoGeneratedCode with ExcludeFromCodeCoverage in
auto-generated Program files by @​nohwnd in
microsoft/vstest#16101
* Include testhost process path in crash error messages by @​nohwnd in
microsoft/vstest#16108
* Fix DataDriven test results being double-counted in TRX logger totals
by @​nohwnd in microsoft/vstest#15766
* Fix datacollector crash visibility: replace Assert with throwable
exceptions by @​nohwnd in microsoft/vstest#16048
* Add TreatErrorMessagesAsWarnings parameter to TRX logger by @​nohwnd
in microsoft/vstest#16106
* Wait for testhost stderr to drain before reading its crash output by
@​nohwnd in microsoft/vstest#16128
* Handle runtimeconfig.dev.json without additionalProbingPaths by @​tmat
in microsoft/vstest#16166
* Suggest Microsoft.NET.Test.Sdk when a managed test project brings no
testhost by @​nohwnd in microsoft/vstest#16169
* Fix x86 testhost loading mismatched x64 hostfxr (0x800700C1) when run
via vstest.console.exe directly (#​16151) by @​azat-msft in
microsoft/vstest#16156
* Preserve the real exception (type + stack trace) when a test run
aborts in BaseRunTests by @​nohwnd in
microsoft/vstest#16167

## New Contributors
* @​drewnoakes made their first contribution in
microsoft/vstest#16045

**Full Changelog**:
microsoft/vstest@v18.8.0...v18.9.0

Commits viewable in [compare
view](microsoft/vstest@v18.8.1...v18.9.0).
</details>

Updated [Umbraco.Cms](https://github.com/umbraco/Umbraco-CMS) from
18.1.0 to 18.1.1.

<details>
<summary>Release notes</summary>

_Sourced from [Umbraco.Cms's
releases](https://github.com/umbraco/Umbraco-CMS/releases)._

## 18.1.1

## What's Changed

### 🔒 Security
* Resolved incorrect authorization lets Content-only backoffice users
modify Templates, enabling remote code execution from
GHSA-f7m5-5x7g-2p52
* Resolved insufficient authorization on Management API search endpoints
from
GHSA-w5q3-9wf8-43gg
 
### 🐛 Bug Fixes
* Routing: Don't retain the fallback default culture captured during an
upgrade boot (closes #​22581) by @​AndyButland in
umbraco/Umbraco-CMS#23653

**Full Changelog**:
umbraco/Umbraco-CMS@release-18.1.0-rc...release-18.1.1

Commits viewable in [compare
view](umbraco/Umbraco-CMS@release-18.1.0...release-18.1.1).
</details>

Updated [uSync](https://github.com/KevinJump/uSync) from 18.0.3 to
18.1.1.

<details>
<summary>Release notes</summary>

_Sourced from [uSync's
releases](https://github.com/KevinJump/uSync/releases)._

## 18.1.1

## Fixes

- **Import**: fixed a property that's been moved out of all groups (an
empty tab, no matching `<Tabs>` entry) not actually persisting that move
on import — it took a second import to stick.
([#​1043](KevinJump/uSync#1043))
- **Reliability**: save failures rejected by Umbraco (e.g. an invalid
ISO code, a rejected content/media/template save) were previously
discarded silently, so a failed import step could still get reported as
a success. These now surface as proper import failures across Language,
DictionaryItem, Webhook, ContentType/MediaType/MemberType, DataType,
Domain, Template, Media, and Content serializers.
([#​1040](KevinJump/uSync#1040),
[#​1042](KevinJump/uSync#1042))
- **Blueprints**: fixed bulk-imported content blueprints being saved
through the wrong path, which silently corrupted their object type and
caused duplicate-key errors on the next import. Blueprints saved during
the second-pass bulk import now go through `SaveBlueprint` like a normal
single-item save.
([#​1035](KevinJump/uSync#1035))

## Other

- Dependency bumps (chalk, vite-plugin-dts, and a few NuGet/npm
dependency-group updates).
- CI/CD: nightly builds are now automatically published to the [Azure
Artifacts nightly
feed](https://pkgs.dev.azure.com/jumoo/Public/_packaging/nightly/nuget/v3/index.json)
on every push to this branch.


## 18.1.0

This is a minor update to uSync for Umbraco 18 — an opt-in import
performance cache, a double-export fix for save-and-publish, several
allocation/perf improvements ported from v17, and a move to the
`Jumoo.Json` package for JSON handling.

## uSync 18.1.0

**Added**
- **Opt-in import state cache — `uSync:Settings:CacheImportState`
(default `false`).** uSync normally decides whether an item has changed
by loading it, serializing it, and hashing it — every item, every run.
With this on, uSync remembers the hash of items it has already confirmed
match, so unchanged items are skipped without a database lookup or
re-serialize on the next run. The first run after enabling it is no
faster than before; the benefit lands on the second run. Read
[`docs/perf/state-cache.md`](https://github.com/KevinJump/uSync/blob/v18/main/docs/perf/state-cache.md)
before enabling it, especially the limitations section.
([#​1016](KevinJump/uSync#1016))
- **Extender API:** the cancelable per-item notifications gain an
optional `Message` (used instead of uSync's generic cancel message) and,
on the import/report ones, `Force`, so a subscriber can stand down when
the user has asked for a forced import.

**Fixed**
- **Content and Library items are no longer exported twice by one editor
action.** Umbraco 18.1 raises the saved notification for a
save-and-publish as well as the published one
([umbraco/Umbraco-CMS#​23523](umbraco/Umbraco-CMS#23523));
uSync now shares a record of exported items across the notifications for
one operation, so each item is exported once.
([#​1018](KevinJump/uSync#1018))
- `HandlerSettings.Clone()` no longer drops `CreateClean` and
`FullFileOnDifference` — handlers that set either value in their own
block are now honoured.
- Property values containing a quote, backslash or control character are
now converted to valid JSON (previously produced invalid JSON in the
string fallback). Inherited with the move to `Jumoo.Json`.
- Restored backoffice guard and content paging fixes that were dropped
in the v17 → v18 merge.
([#​1012](KevinJump/uSync#1012))

**Performance**
- Ported the v17 allocation work: fewer redundant dictionary lookups on
hot paths, and `internal`/`private` classes are now `sealed` so the JIT
can devirtualize their calls. No behavioural changes.
([#​1019](KevinJump/uSync#1019),
[#​1020](KevinJump/uSync#1020))
> **Extender API:** `SyncHandlerRoot.SyncChangeInfo` is now `sealed`
(still `protected`, so handlers can still construct/return one from
`IsItemCurrentAsync`, just not derive from it).
- **Handler settings now inherit from `HandlerDefaults`.** A handler's
own settings block is layered over the set's defaults instead of
replacing them wholesale, so it only needs to specify what it wants to
change. ([#​1001](KevinJump/uSync#1001))
> **Breaking:** a handler block that previously reset settings back to
built-in defaults will now inherit the set's `HandlerDefaults` instead.
Review any set mixing `HandlerDefaults` with per-handler blocks.
- **JSON helpers now come from the `Jumoo.Json` package**, replacing
uSync's own copy which had drifted behind it.
`uSync.Core.Extensions.JsonTextExtensions` still works but is
`[Obsolete]` (removed in v20) — switch to `using Jumoo.Json;`. Note
`TryGetPropertyAsObject` → `TryGetPropertyAsJsonObject`,
`GetPropertyAsObject` → `GetPropertyAsJsonObject`, and missing/null
values now return `null` instead of `string.Empty`.
([#​1014](KevinJump/uSync#1014),
[#​1015](KevinJump/uSync#1015))
- Removed three O(n²) lookups from import (duplicate-key/"keys to keep"
checks now use set lookups; second-pass content/media imports index the
action list once instead of scanning it per item).
([#​1017](KevinJump/uSync#1017))
> Actions updated by a second pass are now updated in place, keeping
their original position in the results list, rather than being moved to
the end. Only display/reporting order is affected.
> **Extender API:** `List<uSyncAction>.CreateActionIndex()` and a
matching `UpdateActions(index, key, handlerAlias, attempt)` overload are
new.
- Serializing, comparing and expanding large property values allocates
far less, some previously on the large object heap. Inherited with the
move to `Jumoo.Json`.

**Extender API**
- `ISyncManagementService` gains `UnpackStreamAsync(Stream)`; the
synchronous `UnpackStream(Stream)` is now obsolete (removed in v19).
([#​1005](KevinJump/uSync#1005))

**Cleanup**
- Cleared the remaining build warnings left over from the Umbraco 18
upgrade: replaced `ITemplate.MasterTemplateAlias` with
`LayoutTemplateAlias`, and inlined the legacy `{localLink:x}` parsing
Umbraco is removing in v18. No behavioural changes.
([#​1002](KevinJump/uSync#1002),
[#​1003](KevinJump/uSync#1003),
[#​1004](KevinJump/uSync#1004))

**Full Changelog**:
KevinJump/uSync@v18.0.3...v18.1.0


Commits viewable in [compare
view](https://github.com/KevinJump/uSync/commits/v18.1.1).
</details>

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Levi Zitting <me@levizitting.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant