XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEExtension.NetCore.AutoConfig

【DLL】自动实现配置文件的存储

公开
关注 0 Fork 0 Star 0
UTF-8
using MessagePack;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Xml.Linq;
using XFEExtension.NetCore.FormatExtension;

namespace XFEExtension.NetCore.AutoConfig;

/// <summary>
/// 配置迁移步骤的上下文。
/// </summary>
public sealed class ProfileMigrationContext
{
    internal ProfileMigrationContext(int fromVersion, ProfileOperationMode operationMode, string content, JsonSerializerOptions jsonOptions)
    {
        FromVersion = fromVersion;
        OperationMode = operationMode;
        Content = content;
        JsonOptions = jsonOptions;
    }

    /// <summary>迁移前版本。</summary>
    public int FromVersion { get; }

    /// <summary>迁移后的版本。</summary>
    public int ToVersion => FromVersion + 1;

    /// <summary>当前存储模式。</summary>
    public ProfileOperationMode OperationMode { get; }

    /// <summary>当前迁移步骤收到的序列化内容。</summary>
    public string Content { get; }

    /// <summary>配置实例使用的 JSON 选项。</summary>
    public JsonSerializerOptions JsonOptions { get; }
}

/// <summary>
/// MessagePack 配置迁移步骤的上下文。属性值保持 MessagePack 二进制表示,只有显式读取或写入时才会反序列化。
/// </summary>
public sealed class MessagePackProfileMigrationContext
{
    private readonly Dictionary<string, byte[]> properties;
    private readonly MessagePackSerializerOptions options;

    internal MessagePackProfileMigrationContext(int fromVersion, Dictionary<string, byte[]> properties, MessagePackSerializerOptions options)
    {
        FromVersion = fromVersion;
        this.properties = properties;
        this.options = options;
    }

    /// <summary>迁移前版本。</summary>
    public int FromVersion { get; }

    /// <summary>迁移后的版本。</summary>
    public int ToVersion => FromVersion + 1;

    /// <summary>当前包含的配置属性名称。</summary>
    public IReadOnlyCollection<string> PropertyNames => properties.Keys;

    /// <summary>判断二进制配置中是否包含指定属性。</summary>
    public bool ContainsProperty(string propertyName)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(propertyName);
        return properties.ContainsKey(propertyName);
    }

    /// <summary>按指定类型读取一个属性。</summary>
    public T? GetProperty<T>(string propertyName)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(propertyName);
        if (!properties.TryGetValue(propertyName, out var content))
            throw new ProfileMigrationException($"MessagePack 配置中不存在属性“{propertyName}”");
        return MessagePackSerializer.Deserialize<T>(content, options);
    }

    /// <summary>写入或替换一个属性。</summary>
    public void SetProperty<T>(string propertyName, T value)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(propertyName);
        properties[propertyName] = MessagePackSerializer.Serialize(value, options);
    }

    /// <summary>删除一个属性。</summary>
    public bool RemoveProperty(string propertyName)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(propertyName);
        return properties.Remove(propertyName);
    }

    /// <summary>重命名一个属性;源属性不存在时不执行操作。</summary>
    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);
    }
}

/// <summary>
/// 配置迁移注册器。每个步骤负责把版本 N 升级到 N + 1。
/// </summary>
public sealed class ProfileMigrationBuilder
{
    private readonly Dictionary<int, List<Func<ProfileMigrationContext, string>>> migrations = [];
    private readonly Dictionary<int, List<Action<MessagePackProfileMigrationContext>>> messagePackMigrations = [];

    /// <summary>
    /// 注册一个结构转换步骤。
    /// </summary>
    public ProfileMigrationBuilder Transform(int fromVersion, Func<ProfileMigrationContext, string> 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;
    }

    /// <summary>
    /// 注册一个 MessagePack 结构转换步骤。未修改的属性不会被反序列化,适合迁移大型对象配置。
    /// </summary>
    public ProfileMigrationBuilder TransformMessagePack(int fromVersion, Action<MessagePackProfileMigrationContext> 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;
    }

    /// <summary>
    /// 注册一个无需修改内容的版本升级步骤。
    /// </summary>
    public ProfileMigrationBuilder NoOp(int fromVersion)
    {
        Transform(fromVersion, static context => context.Content);
        TransformMessagePack(fromVersion, static _ => { });
        return this;
    }

    /// <summary>
    /// 注册跨内置格式的字段重命名步骤。JSON/XML 会同时识别生成的 InstanceXxx 成员名,
    /// MessagePack 会直接重命名属性对应的二进制块。
    /// </summary>
    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<string, byte[]> properties,
        MessagePackSerializerOptions options)
    {
        if (storedVersion < 0)
            throw new ProfileMigrationException($"配置版本不能小于零:{storedVersion}");
        if (storedVersion > targetVersion)
            throw new ProfileMigrationException($"配置文件版本 {storedVersion} 高于当前支持版本 {targetVersion},不支持自动降级");

        var currentProperties = new Dictionary<string, byte[]>(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
    };
}

/// <summary>
/// 配置迁移失败。
/// </summary>
public sealed class ProfileMigrationException : Exception
{
    /// <summary>创建配置迁移异常。</summary>
    public ProfileMigrationException(string message) : base(message) { }

    /// <summary>创建带内部异常的配置迁移异常。</summary>
    public ProfileMigrationException(string message, Exception innerException) : base(message, innerException) { }
}

internal readonly record struct ProfileMigrationResult(string Content, bool WasMigrated);

internal readonly record struct MessagePackProfileMigrationResult(Dictionary<string, byte[]> 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<int>(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
    };
}