< Summary - Jellyfin

Information
Class: Jellyfin.Server.Migrations.Routines.MigrateLinkedChildren
Assembly: jellyfin
File(s): /srv/git/jellyfin/Jellyfin.Server/Migrations/Routines/MigrateLinkedChildren.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 296
Coverable lines: 296
Total lines: 589
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 144
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 5/4/2026 - 12:15:16 AM Line coverage: 0% (0/296) Branch coverage: 0% (0/144) Total lines: 589 5/4/2026 - 12:15:16 AM Line coverage: 0% (0/296) Branch coverage: 0% (0/144) Total lines: 589

Metrics

File(s)

/srv/git/jellyfin/Jellyfin.Server/Migrations/Routines/MigrateLinkedChildren.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.IO;
 4using System.Linq;
 5using System.Text.Json;
 6using Jellyfin.Database.Implementations;
 7using Jellyfin.Database.Implementations.Entities;
 8using Jellyfin.Extensions;
 9using MediaBrowser.Controller;
 10using MediaBrowser.Controller.Library;
 11using Microsoft.EntityFrameworkCore;
 12using Microsoft.Extensions.Logging;
 13using LinkedChildType = Jellyfin.Database.Implementations.Entities.LinkedChildType;
 14
 15namespace Jellyfin.Server.Migrations.Routines;
 16
 17/// <summary>
 18/// Migrates LinkedChildren data from JSON Data column to the LinkedChildren table.
 19/// </summary>
 20[JellyfinMigration("2026-01-13T12:00:00", nameof(MigrateLinkedChildren))]
 21[JellyfinMigrationBackup(JellyfinDb = true)]
 22internal class MigrateLinkedChildren : IDatabaseMigrationRoutine
 23{
 24    private readonly ILogger<MigrateLinkedChildren> _logger;
 25    private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
 26    private readonly ILibraryManager _libraryManager;
 27    private readonly IServerApplicationHost _appHost;
 28    private readonly IServerApplicationPaths _appPaths;
 29
 30    public MigrateLinkedChildren(
 31        ILoggerFactory loggerFactory,
 32        IDbContextFactory<JellyfinDbContext> dbProvider,
 33        ILibraryManager libraryManager,
 34        IServerApplicationHost appHost,
 35        IServerApplicationPaths appPaths)
 36    {
 037        _logger = loggerFactory.CreateLogger<MigrateLinkedChildren>();
 038        _dbProvider = dbProvider;
 039        _libraryManager = libraryManager;
 040        _appHost = appHost;
 041        _appPaths = appPaths;
 042    }
 43
 44    /// <inheritdoc/>
 45    public void Perform()
 46    {
 047        using var context = _dbProvider.CreateDbContext();
 48
 049        var containerTypes = new[]
 050        {
 051            "MediaBrowser.Controller.Entities.Movies.BoxSet",
 052            "MediaBrowser.Controller.Playlists.Playlist",
 053            "MediaBrowser.Controller.Entities.CollectionFolder"
 054        };
 55
 056        var videoTypes = new[]
 057        {
 058            "MediaBrowser.Controller.Entities.Video",
 059            "MediaBrowser.Controller.Entities.Movies.Movie",
 060            "MediaBrowser.Controller.Entities.TV.Episode"
 061        };
 62
 063        var itemsWithData = context.BaseItems
 064            .Where(b => b.Data != null && (containerTypes.Contains(b.Type) || videoTypes.Contains(b.Type)))
 065            .Select(b => new { b.Id, b.Data, b.Type })
 066            .ToList();
 67
 068        _logger.LogInformation("Found {Count} potential items with LinkedChildren data to process.", itemsWithData.Count
 69
 070        var pathToIdMap = context.BaseItems
 071            .Where(b => b.Path != null)
 072            .Select(b => new { b.Id, b.Path })
 073            .GroupBy(b => b.Path!)
 074            .ToDictionary(g => g.Key, g => g.First().Id);
 75
 076        var linkedChildrenToAdd = new List<LinkedChildEntity>();
 077        var processedCount = 0;
 78        const int progressLogStep = 1000;
 079        var totalItems = itemsWithData.Count;
 80
 081        foreach (var item in itemsWithData)
 82        {
 083            if (string.IsNullOrEmpty(item.Data))
 84            {
 85                continue;
 86            }
 87
 088            if (processedCount > 0 && processedCount % progressLogStep == 0)
 89            {
 090                _logger.LogInformation("Processing LinkedChildren: {Processed}/{Total} items", processedCount, totalItem
 91            }
 92
 93            try
 94            {
 095                using var doc = JsonDocument.Parse(item.Data);
 96
 097                var isVideo = videoTypes.Contains(item.Type);
 98
 99                // Handle Video alternate versions
 0100                if (isVideo)
 101                {
 0102                    ProcessVideoAlternateVersions(doc.RootElement, item.Id, pathToIdMap, linkedChildrenToAdd);
 103                }
 104
 105                // Handle LinkedChildren (for containers and other items)
 0106                if (!doc.RootElement.TryGetProperty("LinkedChildren", out var linkedChildrenElement) || linkedChildrenEl
 107                {
 0108                    processedCount++;
 0109                    continue;
 110                }
 111
 0112                var isPlaylist = item.Type == "MediaBrowser.Controller.Playlists.Playlist";
 0113                var sortOrder = 0;
 0114                foreach (var childElement in linkedChildrenElement.EnumerateArray())
 115                {
 0116                    Guid? childId = null;
 0117                    if (childElement.TryGetProperty("ItemId", out var itemIdProp) && itemIdProp.ValueKind != JsonValueKi
 118                    {
 0119                        var itemIdStr = itemIdProp.GetString();
 0120                        if (!string.IsNullOrEmpty(itemIdStr) && Guid.TryParse(itemIdStr, out var parsedId))
 121                        {
 0122                            childId = parsedId;
 123                        }
 124                    }
 125
 0126                    if (!childId.HasValue || childId.Value.IsEmpty())
 127                    {
 0128                        if (childElement.TryGetProperty("Path", out var pathProp))
 129                        {
 0130                            var path = pathProp.GetString();
 0131                            if (!string.IsNullOrEmpty(path) && pathToIdMap.TryGetValue(path, out var resolvedId))
 132                            {
 0133                                childId = resolvedId;
 134                            }
 135                        }
 136                    }
 137
 0138                    if (!childId.HasValue || childId.Value.IsEmpty())
 139                    {
 0140                        if (childElement.TryGetProperty("LibraryItemId", out var libIdProp))
 141                        {
 0142                            var libIdStr = libIdProp.GetString();
 0143                            if (!string.IsNullOrEmpty(libIdStr) && Guid.TryParse(libIdStr, out var parsedLibId))
 144                            {
 0145                                childId = parsedLibId;
 146                            }
 147                        }
 148                    }
 149
 0150                    if (!childId.HasValue || childId.Value.IsEmpty())
 151                    {
 152                        continue;
 153                    }
 154
 0155                    var childType = LinkedChildType.Manual;
 0156                    if (childElement.TryGetProperty("Type", out var typeProp))
 157                    {
 0158                        if (typeProp.ValueKind == JsonValueKind.Number)
 159                        {
 0160                            childType = (LinkedChildType)typeProp.GetInt32();
 161                        }
 0162                        else if (typeProp.ValueKind == JsonValueKind.String)
 163                        {
 0164                            var typeStr = typeProp.GetString();
 0165                            if (Enum.TryParse<LinkedChildType>(typeStr, out var parsedType))
 166                            {
 0167                                childType = parsedType;
 168                            }
 169                        }
 170                    }
 171
 0172                    linkedChildrenToAdd.Add(new LinkedChildEntity
 0173                    {
 0174                        ParentId = item.Id,
 0175                        ChildId = childId.Value,
 0176                        ChildType = childType,
 0177                        SortOrder = isPlaylist ? sortOrder : null
 0178                    });
 179
 0180                    sortOrder++;
 181                }
 182
 0183                processedCount++;
 0184            }
 0185            catch (JsonException ex)
 186            {
 0187                _logger.LogWarning(ex, "Failed to parse JSON for item {ItemId}", item.Id);
 0188            }
 189        }
 190
 0191        if (linkedChildrenToAdd.Count > 0)
 192        {
 0193            _logger.LogInformation("Inserting {Count} LinkedChildren records.", linkedChildrenToAdd.Count);
 194
 0195            var existingKeys = context.LinkedChildren
 0196                .Select(lc => new { lc.ParentId, lc.ChildId })
 0197                .ToHashSet();
 198
 0199            var toInsert = linkedChildrenToAdd
 0200                .Where(lc => !existingKeys.Contains(new { lc.ParentId, lc.ChildId }))
 0201                .ToList();
 202
 0203            if (toInsert.Count > 0)
 204            {
 205                // Deduplicate by composite key (ParentId, ChildId)
 206                // Priority: LocalAlternateVersion > LinkedAlternateVersion > Other
 0207                toInsert = toInsert
 0208                    .OrderBy(lc => lc.ChildType switch
 0209                    {
 0210                        LinkedChildType.LocalAlternateVersion => 0,
 0211                        LinkedChildType.LinkedAlternateVersion => 1,
 0212                        _ => 2
 0213                    })
 0214                    .DistinctBy(lc => new { lc.ParentId, lc.ChildId })
 0215                    .ToList();
 216
 0217                var childIds = toInsert.Select(lc => lc.ChildId).Distinct().ToList();
 0218                var existingChildIds = context.BaseItems
 0219                    .WhereOneOrMany(childIds, b => b.Id)
 0220                    .Select(b => b.Id)
 0221                    .ToHashSet();
 222
 0223                toInsert = toInsert.Where(lc => existingChildIds.Contains(lc.ChildId)).ToList();
 224
 0225                context.LinkedChildren.AddRange(toInsert);
 0226                context.SaveChanges();
 227
 0228                _logger.LogInformation("Successfully inserted {Count} LinkedChildren records.", toInsert.Count);
 229            }
 230            else
 231            {
 0232                _logger.LogInformation("All LinkedChildren records already exist, nothing to insert.");
 233            }
 234        }
 235        else
 236        {
 0237            _logger.LogInformation("No LinkedChildren data found to migrate.");
 238        }
 239
 0240        _logger.LogInformation("LinkedChildren migration completed. Processed {Count} items.", processedCount);
 241
 0242        CleanupWrongTypeAlternateVersions(context);
 0243        CleanupOrphanedAlternateVersionBaseItems(context);
 0244        CleanupItemsFromDeletedLibraries(context);
 0245        CleanupStaleFileEntries(context);
 0246        CleanupOrphanedLinkedChildren(context);
 0247    }
 248
 249    private void CleanupWrongTypeAlternateVersions(JellyfinDbContext context)
 250    {
 0251        _logger.LogInformation("Cleaning up alternate version items with wrong type...");
 252
 253        // Find all LocalAlternateVersion relationships where the child is a generic Video
 254        // but the parent is a more specific type (like Movie).
 255        // Since IDs are computed from type + path, just updating the Type column would break ID lookups.
 256        // Instead, delete them and let the runtime recreate them with the correct type during the next library scan.
 0257        var wrongTypeChildIds = context.LinkedChildren
 0258            .Where(lc => lc.ChildType == LinkedChildType.LocalAlternateVersion)
 0259            .Join(
 0260                context.BaseItems,
 0261                lc => lc.ParentId,
 0262                parent => parent.Id,
 0263                (lc, parent) => new { lc.ChildId, ParentType = parent.Type })
 0264            .Join(
 0265                context.BaseItems,
 0266                x => x.ChildId,
 0267                child => child.Id,
 0268                (x, child) => new { x.ChildId, x.ParentType, ChildType = child.Type })
 0269            .Where(x => x.ChildType != x.ParentType)
 0270            .Select(x => x.ChildId)
 0271            .Distinct()
 0272            .ToList();
 273
 0274        if (wrongTypeChildIds.Count == 0)
 275        {
 0276            _logger.LogInformation("No wrong-type alternate version items found.");
 0277            return;
 278        }
 279
 0280        _logger.LogInformation("Found {Count} wrong-type alternate version items to remove.", wrongTypeChildIds.Count);
 281
 0282        var itemsToDelete = wrongTypeChildIds
 0283            .Select(id => _libraryManager.GetItemById(id))
 0284            .Where(item => item is not null)
 0285            .ToList();
 0286        _libraryManager.DeleteItemsUnsafeFast(itemsToDelete!);
 287
 0288        _logger.LogInformation("Removed {Count} wrong-type alternate version items. They will be recreated with the corr
 0289    }
 290
 291    private void CleanupOrphanedAlternateVersionBaseItems(JellyfinDbContext context)
 292    {
 0293        _logger.LogInformation("Starting cleanup of orphaned alternate version BaseItems...");
 294
 295        // Find BaseItems that have OwnerId set (they belonged to another item) and are not extras,
 296        // but no LinkedChild entry references them — meaning they're orphaned alternate versions.
 297        // This happens when a version file is renamed: the old BaseItem remains in the DB
 298        // with a stale OwnerId but nothing links to it anymore.
 0299        var orphanedVersionIds = context.BaseItems
 0300            .Where(b => b.OwnerId.HasValue && b.ExtraType == null)
 0301            .Where(b => !context.LinkedChildren.Any(lc => lc.ChildId.Equals(b.Id)))
 0302            .Select(b => b.Id)
 0303            .ToList();
 304
 0305        if (orphanedVersionIds.Count == 0)
 306        {
 0307            _logger.LogInformation("No orphaned alternate version BaseItems found.");
 0308            return;
 309        }
 310
 0311        _logger.LogInformation("Found {Count} orphaned alternate version BaseItems to remove.", orphanedVersionIds.Count
 312
 0313        var itemsToDelete = orphanedVersionIds
 0314            .Select(id => _libraryManager.GetItemById(id))
 0315            .Where(item => item is not null)
 0316            .ToList();
 0317        _libraryManager.DeleteItemsUnsafeFast(itemsToDelete!);
 318
 0319        _logger.LogInformation("Removed {Count} orphaned alternate version BaseItems.", itemsToDelete.Count);
 0320    }
 321
 322    private void CleanupItemsFromDeletedLibraries(JellyfinDbContext context)
 323    {
 0324        _logger.LogInformation("Starting cleanup of items from deleted libraries...");
 325
 326        // Find BaseItems whose TopParentId points to a library (collection folder) that no longer exists.
 327        // This happens when a library is removed but the scan didn't fully clean up all items under it.
 0328        var orphanedIds = context.BaseItems
 0329            .Where(b => b.TopParentId.HasValue)
 0330            .Where(b => !context.BaseItems.Any(lib => lib.Id.Equals(b.TopParentId!.Value)))
 0331            .Select(b => b.Id)
 0332            .ToList();
 333
 0334        if (orphanedIds.Count == 0)
 335        {
 0336            _logger.LogInformation("No items from deleted libraries found.");
 0337            return;
 338        }
 339
 0340        _logger.LogInformation("Found {Count} items from deleted libraries to remove.", orphanedIds.Count);
 341
 0342        var itemsToDelete = orphanedIds
 0343            .Select(id => _libraryManager.GetItemById(id))
 0344            .Where(item => item is not null)
 0345            .ToList();
 0346        _libraryManager.DeleteItemsUnsafeFast(itemsToDelete!);
 347
 0348        _logger.LogInformation("Removed {Count} items from deleted libraries.", itemsToDelete.Count);
 0349    }
 350
 351    private void CleanupStaleFileEntries(JellyfinDbContext context)
 352    {
 0353        _logger.LogInformation("Starting cleanup of items with missing files...");
 354
 355        // Get all library media locations and partition into accessible vs inaccessible.
 356        // This mirrors the scanner's safeguard: if a library root is inaccessible
 357        // (e.g. NAS offline), we skip items under it to avoid false deletions.
 0358        var virtualFolders = _libraryManager.GetVirtualFolders();
 0359        var accessiblePaths = new List<string>();
 0360        var inaccessiblePaths = new List<string>();
 361
 0362        foreach (var folder in virtualFolders)
 363        {
 0364            foreach (var location in folder.Locations)
 365            {
 0366                if (Directory.Exists(location) && Directory.EnumerateFileSystemEntries(location).Any())
 367                {
 0368                    accessiblePaths.Add(location);
 369                }
 370                else
 371                {
 0372                    inaccessiblePaths.Add(location);
 0373                    _logger.LogWarning(
 0374                        "Library location {Path} is inaccessible or empty, skipping file existence checks for items unde
 0375                        location);
 376                }
 377            }
 378        }
 379
 0380        var allLibraryPaths = accessiblePaths.Concat(inaccessiblePaths).ToList();
 381
 382        // Get all non-folder, non-virtual items with paths from the DB
 0383        var itemsWithPaths = context.BaseItems
 0384            .Where(b => b.Path != null && b.Path != string.Empty)
 0385            .Where(b => !b.IsFolder && !b.IsVirtualItem)
 0386            .Select(b => new { b.Id, b.Path })
 0387            .ToList();
 388
 0389        var internalMetadataPath = _appPaths.InternalMetadataPath;
 390
 0391        var staleIds = new List<Guid>();
 0392        foreach (var item in itemsWithPaths)
 393        {
 394            // Expand virtual path placeholders (%AppDataPath%, %MetadataPath%) to real paths
 0395            var path = _appHost.ExpandVirtualPath(item.Path!);
 396
 397            // Skip items stored under internal metadata (images, subtitles, trickplay, etc.)
 0398            if (path.StartsWith(internalMetadataPath, StringComparison.OrdinalIgnoreCase))
 399            {
 400                continue;
 401            }
 402
 0403            if (accessiblePaths.Any(p => path.StartsWith(p, StringComparison.OrdinalIgnoreCase)))
 404            {
 405                // Item is under an accessible library location — check if it still exists
 406                // Directory check covers BDMV/DVD items whose Path points to a folder
 0407                if (!File.Exists(path) && !Directory.Exists(path))
 408                {
 0409                    staleIds.Add(item.Id);
 410                }
 411            }
 0412            else if (!allLibraryPaths.Any(p => path.StartsWith(p, StringComparison.OrdinalIgnoreCase)))
 413            {
 414                // Item is not under ANY library location (accessible or not) —
 415                // it's orphaned from all libraries (e.g. media path was removed from config)
 0416                staleIds.Add(item.Id);
 417            }
 418
 419            // Otherwise: item is under an inaccessible location — skip (storage may be offline)
 420        }
 421
 0422        if (staleIds.Count == 0)
 423        {
 0424            _logger.LogInformation("No stale items found.");
 0425            return;
 426        }
 427
 0428        _logger.LogInformation("Found {Count} stale items to remove.", staleIds.Count);
 429
 0430        var itemsToDelete = staleIds
 0431            .Select(id => _libraryManager.GetItemById(id))
 0432            .Where(item => item is not null)
 0433            .ToList();
 0434        _libraryManager.DeleteItemsUnsafeFast(itemsToDelete!);
 435
 0436        _logger.LogInformation("Removed {Count} stale items.", itemsToDelete.Count);
 0437    }
 438
 439    private void CleanupOrphanedLinkedChildren(JellyfinDbContext context)
 440    {
 0441        _logger.LogInformation("Starting cleanup of orphaned LinkedChildren records...");
 442
 443        // Find all LinkedChildren where the ChildId doesn't exist in BaseItems
 0444        var orphanedLinkedChildren = context.LinkedChildren
 0445            .Where(lc => !context.BaseItems.Any(b => b.Id.Equals(lc.ChildId)))
 0446            .ToList();
 447
 0448        if (orphanedLinkedChildren.Count == 0)
 449        {
 0450            _logger.LogInformation("No orphaned LinkedChildren found.");
 0451            return;
 452        }
 453
 0454        _logger.LogInformation("Found {Count} orphaned LinkedChildren records to remove.", orphanedLinkedChildren.Count)
 455
 0456        var orphanedByParent = context.LinkedChildren
 0457            .Where(lc => !context.BaseItems.Any(b => b.Id.Equals(lc.ParentId)))
 0458            .ToList();
 459
 0460        if (orphanedByParent.Count > 0)
 461        {
 0462            _logger.LogInformation("Found {Count} LinkedChildren with non-existent parent.", orphanedByParent.Count);
 0463            orphanedLinkedChildren.AddRange(orphanedByParent);
 464        }
 465
 466        // Remove all orphaned records
 0467        var distinctOrphaned = orphanedLinkedChildren.DistinctBy(lc => new { lc.ParentId, lc.ChildId }).ToList();
 0468        context.LinkedChildren.RemoveRange(distinctOrphaned);
 0469        context.SaveChanges();
 470
 0471        _logger.LogInformation("Successfully removed {Count} orphaned LinkedChildren records.", distinctOrphaned.Count);
 0472    }
 473
 474    private void ProcessVideoAlternateVersions(
 475        JsonElement root,
 476        Guid parentId,
 477        Dictionary<string, Guid> pathToIdMap,
 478        List<LinkedChildEntity> linkedChildrenToAdd)
 479    {
 0480        int sortOrder = 0;
 481
 0482        if (root.TryGetProperty("LocalAlternateVersions", out var localAlternateVersionsElement)
 0483            && localAlternateVersionsElement.ValueKind == JsonValueKind.Array)
 484        {
 0485            foreach (var pathElement in localAlternateVersionsElement.EnumerateArray())
 486            {
 0487                if (pathElement.ValueKind != JsonValueKind.String)
 488                {
 489                    continue;
 490                }
 491
 0492                var path = pathElement.GetString();
 0493                if (string.IsNullOrEmpty(path))
 494                {
 495                    continue;
 496                }
 497
 498                // Try to resolve the path to an ItemId
 0499                if (pathToIdMap.TryGetValue(path, out var childId))
 500                {
 0501                    linkedChildrenToAdd.Add(new LinkedChildEntity
 0502                    {
 0503                        ParentId = parentId,
 0504                        ChildId = childId,
 0505                        ChildType = LinkedChildType.LocalAlternateVersion,
 0506                        SortOrder = sortOrder++
 0507                    });
 508
 0509                    _logger.LogDebug(
 0510                        "Migrating LocalAlternateVersion: Parent={ParentId}, Child={ChildId}, Path={Path}",
 0511                        parentId,
 0512                        childId,
 0513                        path);
 514                }
 515                else
 516                {
 0517                    _logger.LogWarning(
 0518                        "Could not resolve LocalAlternateVersion path to ItemId: {Path} for parent {ParentId}",
 0519                        path,
 0520                        parentId);
 521                }
 522            }
 523        }
 524
 0525        if (root.TryGetProperty("LinkedAlternateVersions", out var linkedAlternateVersionsElement)
 0526            && linkedAlternateVersionsElement.ValueKind == JsonValueKind.Array)
 527        {
 0528            foreach (var linkedChildElement in linkedAlternateVersionsElement.EnumerateArray())
 529            {
 0530                Guid? childId = null;
 531
 532                // Try to get ItemId
 0533                if (linkedChildElement.TryGetProperty("ItemId", out var itemIdProp) && itemIdProp.ValueKind != JsonValue
 534                {
 0535                    var itemIdStr = itemIdProp.GetString();
 0536                    if (!string.IsNullOrEmpty(itemIdStr) && Guid.TryParse(itemIdStr, out var parsedId))
 537                    {
 0538                        childId = parsedId;
 539                    }
 540                }
 541
 542                // Try to get from Path if ItemId not available
 0543                if (!childId.HasValue || childId.Value.IsEmpty())
 544                {
 0545                    if (linkedChildElement.TryGetProperty("Path", out var pathProp))
 546                    {
 0547                        var path = pathProp.GetString();
 0548                        if (!string.IsNullOrEmpty(path) && pathToIdMap.TryGetValue(path, out var resolvedId))
 549                        {
 0550                            childId = resolvedId;
 551                        }
 552                    }
 553                }
 554
 555                // Try LibraryItemId as fallback
 0556                if (!childId.HasValue || childId.Value.IsEmpty())
 557                {
 0558                    if (linkedChildElement.TryGetProperty("LibraryItemId", out var libIdProp))
 559                    {
 0560                        var libIdStr = libIdProp.GetString();
 0561                        if (!string.IsNullOrEmpty(libIdStr) && Guid.TryParse(libIdStr, out var parsedLibId))
 562                        {
 0563                            childId = parsedLibId;
 564                        }
 565                    }
 566                }
 567
 0568                if (!childId.HasValue || childId.Value.IsEmpty())
 569                {
 0570                    _logger.LogWarning("Could not resolve LinkedAlternateVersion child ID for parent {ParentId}", parent
 0571                    continue;
 572                }
 573
 0574                linkedChildrenToAdd.Add(new LinkedChildEntity
 0575                {
 0576                    ParentId = parentId,
 0577                    ChildId = childId.Value,
 0578                    ChildType = LinkedChildType.LinkedAlternateVersion,
 0579                    SortOrder = sortOrder++
 0580                });
 581
 0582                _logger.LogDebug(
 0583                    "Migrating LinkedAlternateVersion: Parent={ParentId}, Child={ChildId}",
 0584                    parentId,
 0585                    childId.Value);
 586            }
 587        }
 0588    }
 589}