Report 16: Element Type Duplication Analysis¶
Date: 2026-02-27¶
Updated: 2026-02-27 Status: ALL 3 ROOT CAUSES FIXED
Summary¶
The target v17 database contained 455 element types. Cross-referencing the source v8 database (via migration output), grid configuration migration, and target DB revealed systematic duplication caused by three overlapping code paths. All three root causes have been fixed (commits 09935d2, 164069f, 76a17cf). Fresh migration runs now produce ~430 element types.
Three Sources of Element Types¶
Source 1: Content Type Migration (v8 → v17 copy)¶
Copies ALL v8 element types with their ORIGINAL aliases and properties. These have low-to-mid nodeIds (1300-2900).
Examples: fancyVideo (1830), headingItem (1315), accordionControls (1348)
Source 2: Grid Configuration Migration (creates grid_element_*)¶
Creates "simplified" element types for each grid editor that doesn't match an existing v8 element type. Uses grid_element_{lowercase_alias} naming. NodeIds in the 3000-3700 range.
Examples: grid_element_psfancyvideo (3351), grid_element_socialfooter (3307), grid_element_largegooglemap (3097)
Source 3: Content Migration Fallback — DefaultGridEditorConverter (creates gridElement*)¶
FILE: GridLayoutToBlockGridMigrator.cs:3659-3701
During CONTENT migration, when processing actual page content, each grid control is processed:
1. Check if DTGE control (has dtgeContentTypeAlias) → use existing element type
2. Try specific converter from _editorConverters map (line 1696)
3. Fall back to DefaultGridEditorConverter (line 1699) which creates gridElement{PascalCase(alias)} WITHOUT checking if grid_element_{alias} already exists.
This creates duplicates with nodeIds 12811-12819, all at the END of migration.
Examples: gridElementLargegooglemap (12811), gridElementSocialfooter (12818), gridElementStickysocial (12819)
Confirmed Duplicate Pairs¶
Category A: grid_element_* + gridElement* (same content, different naming)¶
| grid_element_* (Source 2) | gridElement* (Source 3) | NodeIDs |
|---|---|---|
grid_element_socialfooter (3307) |
gridElementSocialfooter (12818) |
DUPLICATE — Source 2 + Source 3 (DefaultGridEditorConverter using old gridElement naming; fixed to grid_element_ in future runs) |
grid_element_stickysocial (3123) |
gridElementStickysocial (12819) |
DUPLICATE — Source 2 + Source 3 (same DefaultGridEditorConverter naming issue) |
grid_element_largegooglemap (3097) |
gridElementLargegooglemap (12811) |
DUPLICATE |
grid_element_gridsettingsmargineditor (3179) |
gridElementGridsettingsmargineditor (12815) |
TRIPLE (also shouldn't exist) |
grid_element_gridsettingspaddingeditor (3181) |
gridElementGridsettingspaddingeditor (12816) |
TRIPLE (also shouldn't exist) |
grid_element_psdataprotectiondpo (3107) |
gridElementPsdataprotectiondpo (12817) |
DUPLICATE |
grid_element_psdataprotectionname (3109) |
gridElementPsdataprotectionname (12812) |
DUPLICATE |
grid_element_psdataprotectionsummarytable (3113) |
gridElementPsdataprotectionsummarytable (12813) |
DUPLICATE |
grid_element_psdataprotectionweare (3115) |
gridElementPsdataprotectionweare (12814) |
DUPLICATE |
Category B: grid_element_* + original v8 alias (different prefix)¶
| grid_element_* (Source 2) | v8 Original (Source 1) | Root Cause |
|---|---|---|
grid_element_psfancyvideo (3351) |
fancyVideo (1830) |
"ps" prefix not stripped |
grid_element_faq_accordion_home (3585) |
gridElementFaqAccordionHome (migrated from v8) | camelCase vs snake_case |
grid_element_services_list (3121) |
gridElementServicesList (migrated from v8) | camelCase vs snake_case |
Category C: Settings editors (should NOT be element types)¶
| Alias | NodeID | Why Wrong |
|---|---|---|
grid_element_gridsettingsmargineditor |
3179 | Settings editor, not content type |
grid_element_gridsettingspaddingeditor |
3181 | Settings editor, not content type |
gridElementGridsettingsmargineditor |
12815 | Duplicate of above + shouldn't exist |
gridElementGridsettingspaddingeditor |
12816 | Duplicate of above + shouldn't exist |
Element Types by Category¶
v8 Original DTGE Element Types (Source 1, ~150 types)¶
These are correct and should be KEPT. The grid config migration should REUSE these instead of creating new ones.
accordionButtonItem, accordionButtonItemMain, accordionButtonMain, accordionControls,
accordionItem, addressItem, airRange, anchor, announcementControls, announcementItem,
announcementItemScroll, announcementItemScrollControls, barChart, barChartColourPickerItem,
barChartColours, barChartItem, blockListCallToAction, blockListCallToActionSettings,
buttonModal, buttonWidgetControl, calculatorControls, calculatorInputVarRate, calculatorItem,
calculatorItemInput, cardGridControls, cardGridItem, cardItem, careerWidgetControl,
careerWidgetListControl, commonBondItem, comparsionTable, contactUsWidget, coOrdinate,
counter, creditUnionDetails, ctaCurves, cTAItem, ctaText, donutChart, donutChartItem,
donutChartList, facebookControls, fancyVideo, faqControls, fAQItem, faqItems,
faqPickerItem, faqWithSchema, faqWithSchemaList, featureControls, featureItem,
flipCardControls, flipCardItem, footerLinkList, footerLinks, footerListControl,
galleryImageItem, galleryItem, googleMap, gridinternalvideoslide, gridslider,
gridVimeoSlide, header, headerContact, headerContactSetting, headerFull,
headerFullSettings, headerNew, headerNews, headerNewsSettings, headerSettings,
headerSettingsNew, headingItem, heroItem, homepageSpotlight, iconLinkItem,
iconListControl, iconListItem, iconListOptContactInfoControl, iconListOptContactInfoItem,
imageGalleryControl, imageGalleryItem, imageListControl, imageListItem, imageWithLink,
instagramControls, InternalVideoSlide, linkListControl, linkListItem, LinkPicker,
loanBoxContactItem, loanBoxItem, loanBoxWidgetItem, LoanBoxWidgetItemMain, loanInputCTA,
loginStatusWidget, loginWidget, mainCalculatorControls, mainLoanBoxWidget, mediaElement,
newsCategoryItem, newsWidget, numberedListControl, numberedListItem, parallaxImageItem,
pictureSlide, PictureSlideGrid, pictureSlideTextBox, qrCodeItem, quickLinkDropDownControl,
quoteElement, releaseNotesWidget, richTextElement, secondParagraphControl,
secondParagraphItem, sliderItem, sliderItemMp4, sliderItemYoutube, socialLinkItem,
splitPictureSlider, spotlightControl, spotlightControls, spotlightGridControls,
SpotlightGridItem, spotlightItem, staffMemberControl, staffMemberItem, staffMembersControl,
stickyNav, stripedList, stripedListControl, stripedListItem, styledListControl,
styledListItem, testimonialCardItem, textSlide, trustpilotWidget, twitterControls,
videoBreakoutItem, videoFullWidth, videoPopout, VimeoSlide, youTubeControls,
youTubeSlide, youTubeSlideGrid
Grid Infrastructure Types (~170 types)¶
Per-datatype layout, rowconfig, areawrapper, settings types. These are CORRECT and necessary.
Format: grid_{dataTypeId}_{type}_{name}
DataTypes: 1055, 1093, 1127, 1151, 1257, 1459, 1534, 1683, 1709, 1736, 1780, 1798, 1803, 2236
Grid Built-in Types (5 types)¶
Grid Element Types — REVIEW NEEDED (~45 types)¶
These are the grid_element_* types. Each needs to be checked: does it duplicate a v8 original?
| grid_element_* Alias | Matching v8 Type? | Status |
|---|---|---|
| grid_element_anchorwidget | anchor (1858→2285) | POSSIBLE DUPLICATE — check properties |
| grid_element_announcement_scroll | announcementItemScroll? | Check if same |
| grid_element_button | buttonWidgetControl? | Check |
| grid_element_calculatorsgrideditor | calculatorControls? | Check |
| grid_element_cardcontrol | cardItem? cardGridControls? | Check |
| grid_element_faq_accordion_home | (None — v8 had gridElementFaqAccordionHome) | DUPLICATE of v8 gridElement type |
| grid_element_flip_cards | flipCardControls? | Check |
| grid_element_gesocialfeeds | facebookControls? instagramControls? | Check |
| grid_element_globaladdress | googleMap? | Check |
| grid_element_globaladdresscompletegrideditor | (None) | Simplified type, OK |
| grid_element_gridsettingsmargineditor | NONE | DELETE — settings editor |
| grid_element_gridsettingspaddingeditor | NONE | DELETE — settings editor |
| grid_element_herobar | heroItem? | Check |
| grid_element_largegooglemap | googleMap? | Different editor |
| grid_element_largeleafletmap | (None) | Simplified type, OK |
| grid_element_loanboxescompletegrideditor | mainLoanBoxWidget? | Check |
| grid_element_newscompletegrideditor | newsWidget? | Check |
| grid_element_psdataprotectiondpo | (None in v8 with exact alias) | Has gridElement* duplicate |
| grid_element_psdataprotectionname | (None) | Has gridElement* duplicate |
| grid_element_psdataprotectionspecificstatements | (None) | Only 1 version exists |
| grid_element_psdataprotectionsummarytable | (None) | Has gridElement* duplicate |
| grid_element_psdataprotectionweare | (None) | Has gridElement* duplicate |
| grid_element_psfancyvideo | fancyVideo (1830) | DUPLICATE — ps prefix |
| grid_element_psstyledheading | headingItem? | Check |
| grid_element_quicklinksdropdown | quickLinkDropDownControl? | Check |
| grid_element_services_list | (v8 gridElementServicesList) | DUPLICATE of v8 type |
| grid_element_services_list_image | (None) | Simplified type, OK |
| grid_element_socialfooter | (v8 gridElementSocialfooter) | DUPLICATE of v8 type |
| grid_element_stickysocial | (None in v8 original) | Has gridElement* duplicate |
| grid_element_testimonals_half | (None) | Simplified type, OK |
| grid_element_testimonial | testimonialCardItem? | Check |
| grid_element_testimonialcompletegrideditor | (v8 gridElementTestimonialcompletegrideditor) | DUPLICATE of v8 type |
| grid_element_testimonialcontrol | (None) | Simplified type, OK |
| grid_element_testimoniallist | (None) | Simplified type, OK |
| grid_element_testimonials | (None) | Simplified type, OK |
| grid_element_testimonialsgrideditor | (None) | Simplified type, OK |
| grid_element_trustpilotwidgetgrideditor | trustpilotWidget? | Check |
| grid_element_twitter | twitterControls? | Check |
| grid_element_youtubeembed_ | youTubeControls? | Check |
New v17-only Types (5 types)¶
Created by Umbraco 17 for new functionality:
audioTagElement (3127), cookiesElement (3132), loginElement (3129),
loginStatusElement (3130), registerElement (3131), umbracoFormElement (3133)
Root Cause Analysis¶
Why 3 code paths create element types:¶
-
Content Type Migration (
SqlMigrationService.MigrateContentTypesAsync): Reads ALLcmsContentTyperows from v8 whereisElement=1and copies them to v17. This includes DTGE element types likegridElementSocialfooter. -
Grid Configuration Migration (
GridConfigurationMigrationService.CreateContentElementTypesAsync): For each unique grid editor alias across all grid datatypes, creates/reuses an element type. When the editor alias doesn't match any existing type, createsgrid_element_{alias}. -
Content Migration Fallback (
DefaultGridEditorConverterinGridLayoutToBlockGridMigrator.cs:3659-3701): During content migration, createsgridElement{PascalCase(alias)}types with high nodeIds (12811+) WITHOUT checking ifgrid_element_{alias}already exists from Path 2. TheDefaultGridEditorConverterwas fixed to usegrid_element_prefix instead ofgridElementprefix, but this only prevents FUTURE runs from creating thegridElement*variants.
Execution Order Root Cause¶
The migration steps in SqlMigrationService.MigrateAsync execute in this order:
| Step | Method | Creates Element Types? |
|---|---|---|
| 5 | MigrateAllNodesAsync |
Copies umbracoNode rows only (no cmsContentType) |
| 6 | MigrateDataTypeDefinitionsAsync |
No |
| 6.5 | CreateGridLayoutElementTypesAsync |
YES -- creates grid_element_* types |
| 6.6 | MigrateContentTypeDefinitionsAsync |
YES -- copies v8 content types (sets isElement=1) |
| 7 | MigratePropertyDataAsync |
YES -- DefaultGridEditorConverter fallback creates gridElement* |
| 7.3 | MigrateElementTypesAsync |
YES -- persists element types from Step 7 |
The key problem: Step 6.5 searches the TARGET DB for existing element types like fancyVideo, but the cmsContentType row doesn't exist yet -- only the umbracoNode from Step 5. The SQL query joins umbracoNode and cmsContentType with isElement=1, so the lookup returns no match. Step 6.6 hasn't run yet, so v8 element type definitions are not discoverable in the target.
Impact: Even if the alias lookup at Step 6.5 were perfect (with ps-prefix stripping, case normalization, etc.), it would STILL fail against the target DB because the element type definitions haven't been migrated yet. The grid_element_* duplicates are structurally guaranteed by the current execution order.
The real fix: The SOURCE DB lookup (FindSourceElementTypeForGridEditorAsync) -- find element types in the v8 source database where they already exist, rather than the target DB where they haven't been created yet. This avoids the execution order problem entirely.
Example -- socialfooter case:
- Step 6.5 creates
grid_element_socialfooter(nodeId 3307) because target lookup fails - Step 6.6 copies v8 types including original element types to target
- Step 7
DefaultGridEditorConverterwould have createdgridElementSocialfooter(nodeId 12818) -- now fixed by commit164069fto usegrid_element_prefix instead, preventing this third duplicate on future runs
Source DB Lookup Gap¶
FindSourceElementTypeForGridEditorAsync at line 1765 does NOT strip the "ps" prefix. For editor alias psFancyVideo, it tries the following variations but never fancyVideo:
| Variation tried | Example for psFancyVideo |
Result |
|---|---|---|
| Original alias | psFancyVideo |
No match (v8 alias is fancyVideo) |
grid + alias |
gridpsFancyVideo |
No match |
alias + Item |
psFancyVideoItem |
No match |
| Lowercase | psfancyvideo |
No match |
grid + lowercase |
gridpsfancyvideo |
No match |
Never tried: fancyVideo (strip "ps" prefix) — which IS the actual v8 element type alias.
Fix: Add ps-prefix stripping to the alias variations in FindSourceElementTypeForGridEditorAsync. For any alias starting with "ps" (case-insensitive), also try the alias with "ps" stripped and the next character lowercased (e.g., psFancyVideo -> fancyVideo). This is the primary fix -- searching the SOURCE DB avoids the execution order problem entirely.
Why lookups fail (summary):¶
The alias variation lookup at GridConfigurationMigrationService.cs:1534-1542 tries:
- normalizedAlias (e.g., psfancyvideo)
- grid{normalizedAlias}, grid_{normalizedAlias}
- gridElement{strippedAlias} (e.g., gridElementpsfancyvideo)
But it DOESN'T:
- Strip "ps" prefix → misses fancyVideo
- Do case-insensitive full scan → gridElementpsfancyvideo ≠ gridElementPsfancyvideo
- Try the v8 DTGE naming convention with proper casing
- Check at the right time — target DB element types don't exist yet at Step 6.5 (execution order bug)
Fix Plan -- ALL FIXES APPLIED¶
Fix 0: Source DB lookup (ROOT CAUSE) -- FIXED (commit 76a17cf)¶
Status: COMPLETE
Rather than reordering migration steps, the fix queries the SOURCE DB for element types using FindSourceElementTypeForGridEditorAsync. This avoids the execution order problem entirely (Step 6.5 runs before Step 6.6, so target DB lookups fail). The source DB lookup now includes ps-prefix stripping: psFancyVideo correctly resolves to fancyVideo.
Fix 1: Enhanced alias lookup -- FIXED (commit 09935d2)¶
Status: COMPLETE
Added to GridConfigurationMigrationService.cs:
- ps-prefix stripping in target DB lookup
- Case-insensitive SQL queries
- Settings editor exclusion (gridSettingsMarginEditor, gridSettingsPaddingEditor)
Fix 2: Settings editor exclusion -- FIXED (commit 09935d2)¶
Status: COMPLETE
Settings editors (gridSettingsMarginEditor, gridSettingsPaddingEditor) are now excluded from element type creation. Prevents 4 incorrect element types (2 pairs).
Fix 3: DefaultGridEditorConverter prefix -- FIXED (commit 164069f)¶
Status: COMPLETE
The DefaultGridEditorConverter now uses grid_element_ prefix instead of gridElement prefix, and skips settings editors. Prevents future runs from creating gridElement* duplicates of existing grid_element_* types.
Fix 4: Deduplication pass -- DEFERRED¶
Post-migration cleanup. Existing gridElement* types from previous runs remain in the DB. A deduplication pass would normalize aliases, group by normalized alias, merge duplicates, and update BlockGrid content JSON. This is deferred as fresh migration runs no longer produce duplicates.
Metrics¶
| Category | Count |
|---|---|
| v8 Original Element Types | ~150 |
| Grid Infrastructure (layouts, rows, areas, settings) | ~170 |
| Grid Built-in | 5 |
| Grid Element (simplified) | ~45 |
| New v17-only | 6 |
| gridElement* duplicates (high nodeIds) | 9 |
| Confirmed duplicate pairs | ~15 |
| Settings editors (should not exist) | 4 (2 pairs) |
| Total | 455 |
| After dedup | ~430 |