using MessagePack; using System.Text.Json; using System.Text.Json.Nodes; using System.Xml.Linq; using XFEExtension.NetCore.FormatExtension; namespace XFEExtension.NetCore.AutoConfig; /// /// 配置迁移步骤的上下文。 /// public sealed class ProfileMigrationContext { internal ProfileMigrationContext(int fromVersion, ProfileOperationMode operationMode, string content, JsonSerializerOptions jsonOptions) { FromVersion = fromVersion; OperationMode = operationMode; Content = content; JsonOptions = jsonOptions; } /// 迁移前版本。 public int FromVersion { get; } /// 迁移后的版本。 public int ToVersion => FromVersion + 1; /// 当前存储模式。 public ProfileOperationMode OperationMode { get; } /// 当前迁移步骤收到的序列化内容。 public string Content { get; } /// 配置实例使用的 JSON 选项。 public JsonSerializerOptions JsonOptions { get; } } /// /// MessagePack 配置迁移步骤的上下文。属性值保持 MessagePack 二进制表示,只有显式读取或写入时才会反序列化。 /// public sealed class MessagePackProfileMigrationContext { private readonly Dictionary properties; private readonly MessagePackSerializerOptions options; internal MessagePackProfileMigrationContext(int fromVersion, Dictionary properties, MessagePackSerializerOptions options) { FromVersion = fromVersion; this.properties = properties; this.options = options; } /// 迁移前版本。 public int FromVersion { get; } /// 迁移后的版本。 public int ToVersion => FromVersion + 1; /// 当前包含的配置属性名称。 public IReadOnlyCollection PropertyNames => properties.Keys; /// 判断二进制配置中是否包含指定属性。 public bool ContainsProperty(string propertyName) { ArgumentException.ThrowIfNullOrWhiteSpace(propertyName); return properties.ContainsKey(propertyName); } /// 按指定类型读取一个属性。 public T? GetProperty(string propertyName) { ArgumentException.ThrowIfNullOrWhiteSpace(propertyName); if (!properties.TryGetValue(propertyName, out var content)) throw new ProfileMigrationException($"MessagePack 配置中不存在属性“{propertyName}”"); return MessagePackSerializer.Deserialize(content, options); } /// 写入或替换一个属性。 public void SetProperty(string propertyName, T value) { ArgumentException.ThrowIfNullOrWhiteSpace(propertyName); properties[propertyName] = MessagePackSerializer.Serialize(value, options); } /// 删除一个属性。 public bool RemoveProperty(string propertyName) { ArgumentException.ThrowIfNullOrWhiteSpace(propertyName); return properties.Remove(propertyName); } /// 重命名一个属性;源属性不存在时不执行操作。 public void RenameProperty(string oldName, string newName) { ArgumentException.ThrowIfNullOrWhiteSpace(oldName); ArgumentException.ThrowIfNullOrWhiteSpace(newName); if (!properties.TryGetValue(oldName, out var content)) return; if (properties.ContainsKey(newName)) throw new ProfileMigrationException($"无法把 MessagePack 字段“{oldName}”重命名为“{newName}”:目标字段已存在"); properties.Remove(oldName); properties.Add(newName, content); } } /// /// 配置迁移注册器。每个步骤负责把版本 N 升级到 N + 1。 /// public sealed class ProfileMigrationBuilder { private readonly Dictionary>> migrations = []; private readonly Dictionary>> messagePackMigrations = []; /// /// 注册一个结构转换步骤。 /// public ProfileMigrationBuilder Transform(int fromVersion, Func transform) { ArgumentNullException.ThrowIfNull(transform); if (fromVersion < 0) throw new ArgumentOutOfRangeException(nameof(fromVersion)); if (!migrations.TryGetValue(fromVersion, out var transforms)) { transforms = []; migrations.Add(fromVersion, transforms); } transforms.Add(transform); return this; } /// /// 注册一个 MessagePack 结构转换步骤。未修改的属性不会被反序列化,适合迁移大型对象配置。 /// public ProfileMigrationBuilder TransformMessagePack(int fromVersion, Action transform) { ArgumentNullException.ThrowIfNull(transform); if (fromVersion < 0) throw new ArgumentOutOfRangeException(nameof(fromVersion)); if (!messagePackMigrations.TryGetValue(fromVersion, out var transforms)) { transforms = []; messagePackMigrations.Add(fromVersion, transforms); } transforms.Add(transform); return this; } /// /// 注册一个无需修改内容的版本升级步骤。 /// public ProfileMigrationBuilder NoOp(int fromVersion) { Transform(fromVersion, static context => context.Content); TransformMessagePack(fromVersion, static _ => { }); return this; } /// /// 注册跨内置格式的字段重命名步骤。JSON/XML 会同时识别生成的 InstanceXxx 成员名, /// MessagePack 会直接重命名属性对应的二进制块。 /// public ProfileMigrationBuilder RenameProperty(int fromVersion, string oldName, string newName) { ArgumentException.ThrowIfNullOrWhiteSpace(oldName); ArgumentException.ThrowIfNullOrWhiteSpace(newName); Transform(fromVersion, context => RenameProperty(context, oldName, newName)); TransformMessagePack(fromVersion, context => context.RenameProperty(oldName, newName)); return this; } internal ProfileMigrationResult Apply(int storedVersion, int targetVersion, ProfileOperationMode operationMode, string content, JsonSerializerOptions jsonOptions) { if (storedVersion < 0) throw new ProfileMigrationException($"配置版本不能小于零:{storedVersion}"); if (storedVersion > targetVersion) throw new ProfileMigrationException($"配置文件版本 {storedVersion} 高于当前支持版本 {targetVersion},不支持自动降级"); var currentContent = content; for (var version = storedVersion; version < targetVersion; version++) { if (!migrations.TryGetValue(version, out var transforms) || transforms.Count == 0) throw new ProfileMigrationException($"缺少从版本 {version} 到版本 {version + 1} 的迁移步骤"); foreach (var transform in transforms) { var context = new ProfileMigrationContext(version, operationMode, currentContent, jsonOptions); currentContent = transform(context) ?? throw new ProfileMigrationException($"版本 {version} 的迁移步骤返回了 null"); } } return new ProfileMigrationResult(currentContent, storedVersion != targetVersion); } internal MessagePackProfileMigrationResult ApplyMessagePack( int storedVersion, int targetVersion, Dictionary properties, MessagePackSerializerOptions options) { if (storedVersion < 0) throw new ProfileMigrationException($"配置版本不能小于零:{storedVersion}"); if (storedVersion > targetVersion) throw new ProfileMigrationException($"配置文件版本 {storedVersion} 高于当前支持版本 {targetVersion},不支持自动降级"); var currentProperties = new Dictionary(properties, StringComparer.Ordinal); for (var version = storedVersion; version < targetVersion; version++) { if (!messagePackMigrations.TryGetValue(version, out var transforms) || transforms.Count == 0) throw new ProfileMigrationException($"缺少从版本 {version} 到版本 {version + 1} 的 MessagePack 迁移步骤"); foreach (var transform in transforms) transform(new MessagePackProfileMigrationContext(version, currentProperties, options)); } return new MessagePackProfileMigrationResult(currentProperties, storedVersion != targetVersion); } private static string RenameProperty(ProfileMigrationContext context, string oldName, string newName) => context.OperationMode switch { ProfileOperationMode.XFEDictionary => RenameXfeDictionaryProperty(context.Content, oldName, newName), ProfileOperationMode.Json => RenameJsonProperty(context.Content, oldName, newName, context.JsonOptions), ProfileOperationMode.Xml => RenameXmlProperty(context.Content, oldName, newName), _ => throw new ProfileMigrationException("Custom 存储模式需要通过 Transform 注册自定义字段重命名逻辑") }; private static string RenameXfeDictionaryProperty(string content, string oldName, string newName) { XFEDictionary source = content; var hasOldName = source.Any(entry => entry.Header == oldName); if (hasOldName && source.Any(entry => entry.Header == newName)) throw new ProfileMigrationException($"无法把 XFE 字典字段“{oldName}”重命名为“{newName}”:目标字段已存在"); var destination = new XFEDictionary(); foreach (var entry in source) destination.Add(entry.Header == oldName ? newName : entry.Header, entry.Content); return destination.ToString(); } private static string RenameJsonProperty(string content, string oldName, string newName, JsonSerializerOptions jsonOptions) { if (JsonNode.Parse(content, documentOptions: CreateDocumentOptions(jsonOptions)) is not JsonObject root) throw new ProfileMigrationException("JSON 配置根节点必须是对象才能执行字段重命名"); var candidates = new[] { (Old: ApplyNamingPolicy(oldName, jsonOptions.PropertyNamingPolicy), New: ApplyNamingPolicy(newName, jsonOptions.PropertyNamingPolicy)), (Old: ApplyNamingPolicy("Instance" + oldName, jsonOptions.PropertyNamingPolicy), New: ApplyNamingPolicy("Instance" + newName, jsonOptions.PropertyNamingPolicy)) }; foreach (var candidate in candidates) { if (!root.TryGetPropertyValue(candidate.Old, out var value)) continue; if (root.ContainsKey(candidate.New)) throw new ProfileMigrationException($"无法把 JSON 字段“{candidate.Old}”重命名为“{candidate.New}”:目标字段已存在"); root.Remove(candidate.Old); root[candidate.New] = value; break; } return root.ToJsonString(jsonOptions); } private static string RenameXmlProperty(string content, string oldName, string newName) { var document = XDocument.Parse(content, LoadOptions.PreserveWhitespace); var root = document.Root ?? throw new ProfileMigrationException("XML 配置缺少根节点"); var oldNames = new[] { oldName, "Instance" + oldName }; var newNames = new[] { newName, "Instance" + newName }; for (var index = 0; index < oldNames.Length; index++) { var element = root.Elements().FirstOrDefault(candidate => candidate.Name.LocalName == oldNames[index]); if (element is null) continue; if (root.Elements().Any(candidate => candidate.Name.LocalName == newNames[index])) throw new ProfileMigrationException($"无法把 XML 字段“{oldNames[index]}”重命名为“{newNames[index]}”:目标字段已存在"); element.Name = element.Name.Namespace + newNames[index]; break; } return document.ToString(SaveOptions.DisableFormatting); } private static string ApplyNamingPolicy(string name, JsonNamingPolicy? namingPolicy) => namingPolicy?.ConvertName(name) ?? name; private static JsonDocumentOptions CreateDocumentOptions(JsonSerializerOptions options) => new() { AllowTrailingCommas = options.AllowTrailingCommas, CommentHandling = options.ReadCommentHandling == JsonCommentHandling.Skip ? JsonCommentHandling.Skip : JsonCommentHandling.Disallow, MaxDepth = options.MaxDepth }; } /// /// 配置迁移失败。 /// public sealed class ProfileMigrationException : Exception { /// 创建配置迁移异常。 public ProfileMigrationException(string message) : base(message) { } /// 创建带内部异常的配置迁移异常。 public ProfileMigrationException(string message, Exception innerException) : base(message, innerException) { } } internal readonly record struct ProfileMigrationResult(string Content, bool WasMigrated); internal readonly record struct MessagePackProfileMigrationResult(Dictionary Properties, bool WasMigrated); internal static class ProfileVersionMetadata { private const string VersionPropertyName = "$xfeProfileVersion"; private const string XmlVersionAttributeName = "xfeProfileVersion"; public static (int Version, string Content) ReadAndStrip(ProfileOperationMode operationMode, string content, JsonSerializerOptions jsonOptions) => operationMode switch { ProfileOperationMode.XFEDictionary => ReadAndStripXfeDictionary(content), ProfileOperationMode.Json => ReadAndStripJson(content, jsonOptions), ProfileOperationMode.Xml => ReadAndStripXml(content), _ => (0, content) }; public static string Write(ProfileOperationMode operationMode, string content, int version, JsonSerializerOptions jsonOptions) { if (version <= 0) return content; return operationMode switch { ProfileOperationMode.XFEDictionary => WriteXfeDictionary(content, version), ProfileOperationMode.Json => WriteJson(content, version, jsonOptions), ProfileOperationMode.Xml => WriteXml(content, version), _ => content }; } private static (int Version, string Content) ReadAndStripXfeDictionary(string content) { XFEDictionary source = content; var destination = new XFEDictionary(); var version = 0; foreach (var entry in source) { if (entry.Header == VersionPropertyName) { if (!int.TryParse(entry.Content, out version)) throw new ProfileMigrationException($"无效的配置版本:{entry.Content}"); continue; } destination.Add(entry.Header, entry.Content); } return version == 0 && !source.Any(entry => entry.Header == VersionPropertyName) ? (0, content) : (version, destination.ToString()); } private static string WriteXfeDictionary(string content, int version) { XFEDictionary source = content; var destination = new XFEDictionary(); destination.Add(VersionPropertyName, version.ToString(System.Globalization.CultureInfo.InvariantCulture)); foreach (var entry in source) if (entry.Header != VersionPropertyName) destination.Add(entry.Header, entry.Content); return destination.ToString(); } private static (int Version, string Content) ReadAndStripJson(string content, JsonSerializerOptions jsonOptions) { if (JsonNode.Parse(content, documentOptions: CreateDocumentOptions(jsonOptions)) is not JsonObject root) throw new ProfileMigrationException("JSON 配置根节点必须是对象"); var version = 0; if (!root.TryGetPropertyValue(VersionPropertyName, out var versionNode)) return (0, content); if (versionNode is null || !versionNode.AsValue().TryGetValue(out version)) throw new ProfileMigrationException("JSON 配置版本不是有效整数"); root.Remove(VersionPropertyName); return (version, root.ToJsonString(jsonOptions)); } private static string WriteJson(string content, int version, JsonSerializerOptions jsonOptions) { if (JsonNode.Parse(content, documentOptions: CreateDocumentOptions(jsonOptions)) is not JsonObject root) throw new ProfileMigrationException("JSON 配置根节点必须是对象"); root[VersionPropertyName] = version; return root.ToJsonString(jsonOptions); } private static (int Version, string Content) ReadAndStripXml(string content) { var document = XDocument.Parse(content, LoadOptions.PreserveWhitespace); var root = document.Root ?? throw new ProfileMigrationException("XML 配置缺少根节点"); var attribute = root.Attribute(XmlVersionAttributeName); var version = 0; if (attribute is null) return (0, content); if (!int.TryParse(attribute.Value, out version)) throw new ProfileMigrationException($"无效的 XML 配置版本:{attribute.Value}"); attribute.Remove(); return (version, document.ToString(SaveOptions.DisableFormatting)); } private static string WriteXml(string content, int version) { var document = XDocument.Parse(content, LoadOptions.PreserveWhitespace); var root = document.Root ?? throw new ProfileMigrationException("XML 配置缺少根节点"); root.SetAttributeValue(XmlVersionAttributeName, version); return document.ToString(SaveOptions.DisableFormatting); } private static JsonDocumentOptions CreateDocumentOptions(JsonSerializerOptions options) => new() { AllowTrailingCommas = options.AllowTrailingCommas, CommentHandling = options.ReadCommentHandling == JsonCommentHandling.Skip ? JsonCommentHandling.Skip : JsonCommentHandling.Disallow, MaxDepth = options.MaxDepth }; }