{"record":{"id":"91c5092969d25a54","repo":"elsa-workflows/elsa-core","slug":"adapter-adaptertype-settings-migration-contains-a-cycle","errorCode":null,"errorMessage":"Adapter '{adapterType}' settings migration contains a cycle.","messagePattern":"Adapter '(.+?)' settings migration contains a cycle\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"src/modules/Elsa.ExternalAuthentication/Services/AdapterSettingsMigrationService.cs","lineNumber":44,"sourceCode":"            throw new InvalidOperationException($\"The adapter type '{adapterType}' is not installed or deployment-allowed.\");\n\n        var currentVersion = adapter.Describe().SettingsVersion;\n        if (settingsVersion <= 0 || settingsVersion > currentVersion)\n            throw new InvalidOperationException($\"Settings version {settingsVersion} is not compatible with adapter '{adapterType}' version {currentVersion}.\");\n        if (settingsVersion == currentVersion)\n            return new(currentVersion, settings.Clone(), false);\n\n        var migrated = settings.Clone();\n        var version = settingsVersion;\n        var stepCount = 0;\n        while (version < currentVersion)\n        {\n            if (!_migrations.TryGetValue((adapterType, version), out var migration))\n                throw new InvalidOperationException($\"Adapter '{adapterType}' does not provide a settings migration from version {version}.\");\n            if (migration.ToVersion <= version || migration.ToVersion > currentVersion)\n                throw new InvalidOperationException($\"Adapter '{adapterType}' has an invalid settings migration from version {version} to {migration.ToVersion}.\");\n            if (++stepCount > 64)\n                throw new InvalidOperationException($\"Adapter '{adapterType}' settings migration contains a cycle.\");\n\n            migrated = (await migration.MigrateAsync(migrated, cancellationToken)).Clone();\n            version = migration.ToVersion;\n        }\n\n        return new(version, migrated, true);\n    }\n\n    private static IReadOnlyDictionary<(string AdapterType, int FromVersion), IAdapterSettingsMigration> BuildMigrationIndex(\n        IEnumerable<IAdapterSettingsMigration> migrations)\n    {\n        var result = new Dictionary<(string AdapterType, int FromVersion), IAdapterSettingsMigration>();\n        foreach (var migration in migrations)\n        {\n            if (string.IsNullOrWhiteSpace(migration.AdapterType) || migration.FromVersion <= 0)\n                throw new InvalidOperationException(\"Adapter settings migrations must define an adapter type and a positive source version.\");\n            if (!result.TryAdd((migration.AdapterType, migration.FromVersion), migration))\n                throw new InvalidOperationException($\"Adapter '{migration.AdapterType}' registers more than one migration from version {migration.FromVersion}.\");","sourceCodeStart":26,"sourceCodeEnd":62,"githubUrl":"https://github.com/elsa-workflows/elsa-core/blob/fe9217bdfa0e27f0e09e45006eb6898f616e513d/src/modules/Elsa.ExternalAuthentication/Services/AdapterSettingsMigrationService.cs#L26-L62","documentation":"As a loop guard, MigrateAsync caps migration steps at 64. Since every valid migration strictly increases the version, exceeding 64 steps can only happen if the migration graph loops (or the invariant check failed), so the service aborts instead of spinning forever.","triggerScenarios":"A migration graph where (adapterType, version) lookups cycle between versions — normally prevented by the ToVersion > version check, so hitting this indicates corrupted registration state or a mutated registry during execution.","commonSituations":"Custom/buggy registry implementations that return inconsistent migrations between iterations; manually composed migration chains with duplicate overlapping steps; extremely long legitimate chains (>64 versions) in heavily-iterated adapters.","solutions":["Inspect the registered migrations for the adapter and remove/repair the cycle.","If the chain is legitimately long, split the migration or raise the step cap consciously.","Re-register a clean, strictly-increasing migration set at startup."],"exampleFix":"// before\n// cycles 1->2, 2->1\n// after\n// chain 1->2, 2->3 (strictly increasing)","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try { await service.MigrateAsync(type, v, settings); }\ncatch (InvalidOperationException ex) when (ex.Message.Contains(\"contains a cycle\"))\n{ /* dump registered migration graph for diagnosis; abort migration */ }","preventionTips":["Keep the migration registry immutable after startup","Enforce strictly increasing versions so cycles are impossible","Add a graph-cycle unit test for each adapter's migrations"],"tags":["migration","versioning","infinite-loop"],"backgroundTag":"internal-invariant-violation","analyzedSha":"fe9217bdfa0e27f0e09e45006eb6898f616e513d","analyzedAt":"2026-09-13T20:32:34.702Z","contentChangedAt":"2026-09-13T20:32:34.702Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}