Changing an existing IList<T> property (where T : BlockData, a "block list" / nested list property) from a shared property to [CultureSpecific] does not migrate previously saved data. Existing list-item property rows in tblContentProperty keep the BranchSpecificScope value that was correct under the old (shared) schema. Because the CMS load query treats BranchSpecificScope = 0 rows as visible for any requested language, those stale rows continue to leak into every language after the property becomes culture-specific — including any new language branch created afterwards. The net effect: adding a translation to already-existing content silently fails to display correctly, while the same list property on brand-new content (created after the schema change) works as expected. This makes the bug very difficult to spot, because it only reproduces on pre-existing content, not on freshly authored content of the same type.
Environment
Optimizely CMS (EPiServer.CMS.Core) 12.23.1, SQL Server content provider (default).
Also inspected the equivalent code paths in EPiServer.CMS.Core 12.4.0 through 12.24.1 (decompiled) and the relevant methods/SQL are unchanged across that whole range, so this is not specific to one patch version.
.NET 10 / ASP.NET Core, but the affected code lives entirely in the SQL content provider layer and is runtime-agnostic.
Deploy the model above. Create a CardListBlock instance in the master language (e.g. en), add 2–3 CardItemBlock items with Title/IconName filled in, and publish.
This writes rows to tblContentProperty for CardItemBlock.Title / CardItemBlock.IconName with BranchSpecificScope = 0 (list container was shared at save time).
Add [CultureSpecific] to CardListBlock.Cards and deploy. Let Optimizely's content-type sync run (this flips tblPropertyDefinition.LanguageSpecific for the Cards property from 2 (shared) to 4 (culture-specific) — confirmed via tblPropertyDefinition.Saved timestamp). No data migration happens as part of this.
In the CMS edit view, create a new language branch (e.g. no) for the same, already-existingCardListBlock instance from step 1, translate the CardItemBlock.Title values, and publish.
Load the content in the new language (no) via IContentRepository.Get<CardListBlock>(link, norwegianCulture).
Expected result
Each CardItemBlock.Title in the no version reads the Norwegian translation entered in step 3.
Actual result
The Title you get back is non-deterministic / effectively duplicated: the load query returns two rows per list index for Title — the new Norwegian row and the original English row (because the English row is still flagged BranchSpecificScope = 0, which the load query treats as visible under any language) — and whichever one the in-memory hydration keeps last wins. In our reproduction it consistently kept the original (English) value, i.e. the Norwegian edit view shows the correct translation, but the rendered no page shows the English text, as if the translation had never been saved.
Now repeat the same 4 steps but create a brand-newCardListBlock from scratch after step 2 (i.e. [CultureSpecific] is already in place before any data exists) — translations behave correctly. The defect is specific to content that already had list items before the property became culture-specific.
Root cause (from decompiled EPiServer.dll 12.23.1)
BranchSpecificScope is stamped once, from the list container property's currentIsLanguageSpecific value at the moment of that particular save, and that single value is then inherited by every nested sub-property of every list item saved in that operation (it is not re-derived per sub-property, and not re-derived from what the property "should" be under the current schema — only from whatever the in-memory PropertyData.IsLanguageSpecific happens to be for that save).
Crucially, a few lines earlier in the same method:
Rows are only written for properties that are IsModified in the current save. When you add a new language branch and translate Title, the list items belonging to the other, already existing language branch are not modified in that save, so they are never revisited, and their BranchSpecificScope (set back when the container was still shared) is never brought up to date.
Read path — the embedded netContentLoadScopedProperties.sql used to load a scoped/list property:
...
WHERE tblContentProperty.fkContentID =@ContentIDAND ((tblContentProperty.fkPropertyDefinitionID =@PropertyDefinitionIDAND tblContentProperty.ScopeName ISNULL)
OR tblContentProperty.ScopeName LIKE@ScopeName+'[.(:]%')
ANDNOT tblPropertyDefinition.fkContentTypeID ISNULLAND
(@LanguageBranchIDISNULLOR tblContentProperty.fkLanguageBranchID =@LanguageBranchIDOR tblContentProperty.BranchSpecificScope =0)
A row with BranchSpecificScope = 0 is returned for every requested @LanguageBranchID, regardless of which language branch it actually belongs to — that is by design, it's how a shared property value is made visible from any language. A row with BranchSpecificScope = 1 is only returned when fkLanguageBranchID matches the requested language exactly.
Once a list container becomes [CultureSpecific], newly written rows get BranchSpecificScope = 1 and behave correctly (exact-language match only). But rows written before that change keep BranchSpecificScope = 0, so they keep being returned for every language — including the new one — alongside the freshly translated rows for the same list index/property, producing two conflicting values where exactly one is expected.
Direct database evidence
Querying tblContentProperty for the CardItemBlock.Title property (fkPropertyDefinitionID resolved dynamically) on one real, affected content item, before vs. after translating it, shows the inconsistency directly:
SELECT cp.fkContentID, cp.ListIndex, cp.fkLanguageBranchID, cp.BranchSpecificScope, cp.String
FROM tblContentProperty cp
WHERE cp.fkPropertyDefinitionID =@TitlePropertyDefinitionIdAND cp.fkContentID =@ContentIdORDERBY cp.ListIndex, cp.fkLanguageBranchID;
ListIndex
fkLanguageBranchID
BranchSpecificScope
String (Title)
0
1 (en, pre-existing)
0
"Sales start"
0
2 (no, just added)
1
"Salgsstart"
Both rows are returned when loading the content for LanguageBranchID = 2 (no), because the en row's BranchSpecificScope = 0 makes it match any language — even though Title is [CultureSpecific] and the property definition's LanguageSpecific flag is correctly 4 (culture-specific) in tblPropertyDefinition. The schema-level metadata is correct; the already-persisted data is not.
For comparison, the exact same query against a CardListBlock created after the property became [CultureSpecific] shows every row — across every property, every list index, every language, including ones that are themselves shared sub-properties like IconName — uniformly stamped BranchSpecificScope = 1, because every row was written under the current schema in one save. That confirms the value is inherited from the container's current IsLanguageSpecific at save time, not computed from the schema at load time.
Impact
Any content type with an IList<T> (T : BlockData) property that was changed between shared and culture-specific after content already existed with populated list items is affected.
The failure is silent: no exception, no warning in logs, no visual difference in the edit view (both language versions show their own, correctly saved text in the editor) — it only shows up on the rendered/loaded value, and only for content that pre-dated the schema change. This makes it very easy to conclude "our code is wrong" rather than suspecting stored data, especially since a brand-new instance of the exact same content type behaves correctly.
We are not aware of any supported API to inspect or correct BranchSpecificScope — it is not exposed via PropertyData, IContent, or any public repository interface we could find; the only way we found to remediate existing content was a raw SQL UPDATE against tblContentProperty, which is obviously not something we want customers to have to do.
Suggested fix
When a property definition's LanguageSpecific flag changes (in either direction) for a property that is an IList<BlockData> container, run a one-time migration (as part of content type synchronization) that rewrites BranchSpecificScope on every existing tblContentProperty/list-item row under that container to match the new value — mirroring what already happens automatically for any content saved after the change.
Short of a full migration, at minimum log a warning during content type sync when this situation is detected (an IList<BlockData> property changing LanguageSpecific on a property definition that already has existing tblContentProperty rows), so implementers know a manual data fix is required instead of silently leaving inconsistent data behind.
Expose a supported, public way to inspect/repair BranchSpecificScope (e.g. via IPropertyDefinitionRepository or a dedicated maintenance API), so this doesn't require raw SQL against internal tables to fix.
Changing an existing IList<T> property (where T : BlockData, a "block list" / nested list property) from a shared property to [CultureSpecific] does not migrate previously saved data. Existing list-item property rows in tblContentProperty keep the BranchSpecificScope value that was correct under the old (shared) schema. Because the CMS load query treats BranchSpecificScope = 0 rows as visible for any requested language, those stale rows continue to leak into every language after the property becomes culture-specific — including any new language branch created afterwards. The net effect: adding a translation to already-existing content silently fails to display correctly, while the same list property on brand-new content (created after the schema change) works as expected. This makes the bug very difficult to spot, because it only reproduces on pre-existing content, not on freshly authored content of the same type.
Environment
Reproduction
Content model
[ContentType(GUID = "3B2C1A10-0000-0000-0000-000000000001")] public class CardItemBlock : BlockData { [CultureSpecific] public virtual string Title { get; set; } // translatable public virtual string IconName { get; set; } // intentionally shared — icons aren't translated } [ContentType(GUID = "3B2C1A10-0000-0000-0000-000000000002")] public class CardListBlock : BlockWithHeading { // Step 1 of the repro: property starts out WITHOUT [CultureSpecific]. public virtual IList<CardItemBlock> Cards { get; set; } }Steps
Expected result
Each CardItemBlock.Title in the no version reads the Norwegian translation entered in step 3.
Actual result
The Title you get back is non-deterministic / effectively duplicated: the load query returns two rows per list index for Title — the new Norwegian row and the original English row (because the English row is still flagged BranchSpecificScope = 0, which the load query treats as visible under any language) — and whichever one the in-memory hydration keeps last wins. In our reproduction it consistently kept the original (English) value, i.e. the Norwegian edit view shows the correct translation, but the rendered no page shows the English text, as if the translation had never been saved.
Now repeat the same 4 steps but create a brand-new CardListBlock from scratch after step 2 (i.e. [CultureSpecific] is already in place before any data exists) — translations behave correctly. The defect is specific to content that already had list items before the property became culture-specific.
Root cause (from decompiled EPiServer.dll 12.23.1)
Write path — EPiServer.DataAccess.Internal.ContentSaveDB.PopulateUpdateCommands:
IPropertyListItemAccessor propertyListItemAccessor = prop as IPropertyListItemAccessor; if (propertyListItemAccessor != null) { PopulateCommandContext context2 = context; context2.BranchSpecificScope = context.BranchSpecificScope ?? prop.IsLanguageSpecific; context2.Index = null; context2.ScopeName = context.ScopeName ?? "."; PopulateListCommands(context2, prop, properties, updateCommands, propertyListItemAccessor); }BranchSpecificScope is stamped once, from the list container property's current IsLanguageSpecific value at the moment of that particular save, and that single value is then inherited by every nested sub-property of every list item saved in that operation (it is not re-derived per sub-property, and not re-derived from what the property "should" be under the current schema — only from whatever the in-memory PropertyData.IsLanguageSpecific happens to be for that save).
Crucially, a few lines earlier in the same method:
if ((prop.PropertyDefinitionID == 0 && prop.Type != PropertyDataType.Block) || !prop.IsModified) { return; }Rows are only written for properties that are IsModified in the current save. When you add a new language branch and translate Title, the list items belonging to the other, already existing language branch are not modified in that save, so they are never revisited, and their BranchSpecificScope (set back when the container was still shared) is never brought up to date.
Read path — the embedded netContentLoadScopedProperties.sql used to load a scoped/list property:
... WHERE tblContentProperty.fkContentID = @ContentID AND ((tblContentProperty.fkPropertyDefinitionID = @PropertyDefinitionID AND tblContentProperty.ScopeName IS NULL) OR tblContentProperty.ScopeName LIKE @ScopeName + '[.(:]%') AND NOT tblPropertyDefinition.fkContentTypeID IS NULL AND (@LanguageBranchID IS NULL OR tblContentProperty.fkLanguageBranchID = @LanguageBranchID OR tblContentProperty.BranchSpecificScope = 0)A row with BranchSpecificScope = 0 is returned for every requested @LanguageBranchID, regardless of which language branch it actually belongs to — that is by design, it's how a shared property value is made visible from any language. A row with BranchSpecificScope = 1 is only returned when fkLanguageBranchID matches the requested language exactly.
Once a list container becomes [CultureSpecific], newly written rows get BranchSpecificScope = 1 and behave correctly (exact-language match only). But rows written before that change keep BranchSpecificScope = 0, so they keep being returned for every language — including the new one — alongside the freshly translated rows for the same list index/property, producing two conflicting values where exactly one is expected.
Direct database evidence
Querying tblContentProperty for the CardItemBlock.Title property (fkPropertyDefinitionID resolved dynamically) on one real, affected content item, before vs. after translating it, shows the inconsistency directly:
SELECT cp.fkContentID, cp.ListIndex, cp.fkLanguageBranchID, cp.BranchSpecificScope, cp.String FROM tblContentProperty cp WHERE cp.fkPropertyDefinitionID = @TitlePropertyDefinitionId AND cp.fkContentID = @ContentId ORDER BY cp.ListIndex, cp.fkLanguageBranchID;Both rows are returned when loading the content for LanguageBranchID = 2 (no), because the en row's BranchSpecificScope = 0 makes it match any language — even though Title is [CultureSpecific] and the property definition's LanguageSpecific flag is correctly 4 (culture-specific) in tblPropertyDefinition. The schema-level metadata is correct; the already-persisted data is not.
For comparison, the exact same query against a CardListBlock created after the property became [CultureSpecific] shows every row — across every property, every list index, every language, including ones that are themselves shared sub-properties like IconName — uniformly stamped BranchSpecificScope = 1, because every row was written under the current schema in one save. That confirms the value is inherited from the container's current IsLanguageSpecific at save time, not computed from the schema at load time.
Impact
Suggested fix