Skip to content

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_builtin_embed, grid_builtin_media, grid_builtin_quote, grid_builtin_rte, grid_builtin_submenu

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:

  1. Content Type Migration (SqlMigrationService.MigrateContentTypesAsync): Reads ALL cmsContentType rows from v8 where isElement=1 and copies them to v17. This includes DTGE element types like gridElementSocialfooter.

  2. 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, creates grid_element_{alias}.

  3. Content Migration Fallback (DefaultGridEditorConverter in GridLayoutToBlockGridMigrator.cs:3659-3701): During content migration, creates gridElement{PascalCase(alias)} types with high nodeIds (12811+) WITHOUT checking if grid_element_{alias} already exists from Path 2. The DefaultGridEditorConverter was fixed to use grid_element_ prefix instead of gridElement prefix, but this only prevents FUTURE runs from creating the gridElement* 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:

  1. Step 6.5 creates grid_element_socialfooter (nodeId 3307) because target lookup fails
  2. Step 6.6 copies v8 types including original element types to target
  3. Step 7 DefaultGridEditorConverter would have created gridElementSocialfooter (nodeId 12818) -- now fixed by commit 164069f to use grid_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 → gridElementpsfancyvideogridElementPsfancyvideo - 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
Migration documentation by Double for Progress Credit Union