// Copyright (c) 2026 GeWuYou // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. namespace GFramework.Game.Internal; /// /// 提供版本化数据迁移链的共享执行逻辑。 /// /// /// 该运行器只负责“按版本号推进”的公共约束,包括: /// 前向注册校验、缺失链路失败、声明目标版本与实际结果版本一致性,以及避免版本回退或死循环。 /// 它不关心具体存储、日志、回写或异常吞吐策略;这些由调用方负责。 /// internal static class VersionedMigrationRunner { /// /// 复用一次迁移链执行期间不会变化的上下文,避免多个 helper 重复传递同一组委托和消息元数据。 /// /// 迁移数据类型。 /// 迁移描述类型。 private readonly record struct MigrationExecutionContext( Func GetVersion, Func GetToVersion, Func ApplyMigration, string SubjectName, string MigrationKind) where TData : class; /// /// 校验迁移注册是否表示一次有效的前向升级。 /// /// 迁移所作用的主体名称,例如设置类型或存档类型。 /// 用于异常消息的迁移类别名称。 /// 源版本。 /// 目标版本。 /// 异常中要使用的参数名。 /// 目标版本不大于源版本时抛出。 internal static void ValidateForwardOnlyRegistration( string subjectName, string migrationKind, int fromVersion, int toVersion, string paramName) { if (toVersion <= fromVersion) { throw new ArgumentException( $"{migrationKind} for {subjectName} must advance the version number.", paramName); } } /// /// 按目标运行时版本执行连续迁移。 /// /// 迁移数据类型。 /// 迁移描述类型。 /// 原始加载的数据。 /// 当前运行时支持的目标版本。 /// 从数据对象提取版本号的委托。 /// 根据当前版本查找下一步迁移器的委托。 /// 从迁移器提取声明目标版本的委托。 /// 执行单步迁移的委托。 /// 迁移主体名称,用于异常消息。 /// 迁移类别名称,用于异常消息。 /// 迁移到目标版本后的数据;如果已经是最新版本,则返回原对象。 /// /// 、 /// 时抛出。 /// /// /// 为空白时抛出。 /// /// /// 数据版本高于当前运行时、迁移链缺失、迁移器返回 、 /// 迁移结果版本与声明不一致、版本未前进或超出目标版本时抛出。 /// internal static TData MigrateToTargetVersion( TData data, int targetVersion, Func getVersion, Func resolveMigration, Func getToVersion, Func applyMigration, string subjectName, string migrationKind) where TData : class { ArgumentNullException.ThrowIfNull(data); ArgumentNullException.ThrowIfNull(getVersion); ArgumentNullException.ThrowIfNull(resolveMigration); ArgumentNullException.ThrowIfNull(getToVersion); ArgumentNullException.ThrowIfNull(applyMigration); ArgumentException.ThrowIfNullOrWhiteSpace(subjectName); ArgumentException.ThrowIfNullOrWhiteSpace(migrationKind); var currentVersion = getVersion(data); EnsureRuntimeVersionIsSupported(currentVersion, targetVersion, subjectName); if (currentVersion == targetVersion) { return data; } var context = new MigrationExecutionContext( getVersion, getToVersion, applyMigration, subjectName, migrationKind); var current = data; while (currentVersion < targetVersion) { var migration = GetRequiredMigration(resolveMigration, currentVersion, in context); var result = ApplyMigrationStep( migration, current, currentVersion, targetVersion, in context); current = result.Data; currentVersion = result.Version; } return current; } /// /// 拒绝比当前运行时更高的数据版本,避免迁移器在未知版本上继续执行。 /// /// 数据当前版本。 /// 运行时支持的目标版本。 /// 迁移主体名称。 /// 数据版本高于运行时版本时抛出。 private static void EnsureRuntimeVersionIsSupported(int currentVersion, int targetVersion, string subjectName) { if (currentVersion > targetVersion) { throw new InvalidOperationException( $"{subjectName} is version {currentVersion}, which is newer than the current runtime version {targetVersion}."); } } /// /// 解析当前版本必须存在的下一步迁移器,避免在调用循环中重复拼接相同错误。 /// /// 迁移数据类型。 /// 迁移描述类型。 /// 迁移解析委托。 /// 当前版本。 /// 本轮迁移链共享的执行上下文。 /// 已解析的迁移器。 /// 缺少迁移器时抛出。 private static TMigration GetRequiredMigration( Func resolveMigration, int currentVersion, in MigrationExecutionContext context) where TData : class { var migration = resolveMigration(currentVersion); if (migration is null) { throw new InvalidOperationException( $"No {context.MigrationKind} is registered for {context.SubjectName} from version {currentVersion}."); } return migration; } /// /// 执行单步迁移并验证声明目标版本、结果版本与运行时上限之间的一致性。 /// /// 迁移数据类型。 /// 迁移描述类型。 /// 当前步骤的迁移器。 /// 迁移前数据。 /// 迁移前版本。 /// 运行时目标版本。 /// 本轮迁移链共享的执行上下文。 /// 迁移后的数据与新版本号。 /// /// 迁移结果为空、声明目标版本不匹配、版本未前进或超出运行时版本时抛出。 /// private static (TData Data, int Version) ApplyMigrationStep( TMigration migration, TData current, int currentVersion, int targetVersion, in MigrationExecutionContext context) where TData : class { var migratedData = context.ApplyMigration(migration, current) ?? throw new InvalidOperationException( $"{context.MigrationKind} for {context.SubjectName} from version {currentVersion} returned null."); var migratedVersion = context.GetVersion(migratedData); ValidateMigrationResult( currentVersion, targetVersion, migratedVersion, context.GetToVersion(migration), in context); return (migratedData, migratedVersion); } /// /// 校验单步迁移结果与声明目标版本一致,并确保版本严格单调递增且不越过运行时版本。 /// /// 迁移数据类型。 /// 迁移描述类型。 /// 迁移前版本。 /// 运行时目标版本。 /// 迁移后实际版本。 /// 迁移器声明的目标版本。 /// 本轮迁移链共享的执行上下文。 /// /// 声明目标版本不匹配、版本未前进或超出运行时版本时抛出。 /// private static void ValidateMigrationResult( int currentVersion, int targetVersion, int migratedVersion, int declaredTargetVersion, in MigrationExecutionContext context) where TData : class { if (declaredTargetVersion != migratedVersion) { throw new InvalidOperationException( $"{context.MigrationKind} for {context.SubjectName} declared target version {declaredTargetVersion} " + $"but returned version {migratedVersion}."); } if (migratedVersion <= currentVersion) { throw new InvalidOperationException( $"{context.MigrationKind} for {context.SubjectName} must advance beyond version {currentVersion}."); } if (migratedVersion > targetVersion) { throw new InvalidOperationException( $"{context.MigrationKind} for {context.SubjectName} produced version {migratedVersion}, " + $"which exceeds the current runtime version {targetVersion}."); } } }