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

XFEExtension.NetCore.AutoConfig

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

公开
关注 0 Fork 0 Star 0
UTF-8

XFEExtension.NetCore.AutoConfig

NuGet NuGet Downloads License: MIT .NET

📖 English | 简体中文

描述

XFEExtension.NetCore.AutoConfig 是一个基于 Roslyn 增量源生成器的 .NET 库,可以自动为继承自 XFEProfile 的配置文件类生成属性、加载/保存方法,实现配置文件的自动持久化存储。

快速开始

安装

dotnet add package XFEExtension.NetCore.AutoConfig

基础用法

为字段添加 [ProfileProperty] 特性,框架将自动生成对应的静态属性,并在赋值时自动保存配置:

// 创建配置文件类
[AutoLoadProfile]
partial class SystemProfile : XFEProfile
{
    [ProfileProperty]
    string name = string.Empty;

    [ProfileProperty]
    int _age;
}

// 使用配置文件
class Program
{
    static void Main(string[] args)
    {
        SystemProfile.Name = "Test"; // 赋值时自动保存
        Console.WriteLine(SystemProfile.Name);
        Console.WriteLine(SystemProfile.Age); // 下次启动自动读取上次保存的值
    }
}

说明: 配置默认会自动加载。使用 [AutoLoadProfile(false)] 时仍会创建并初始化 Current,但不会读取文件;之后可显式调用 LoadProfile()

自动保存默认会在 100ms 窗口内合并。同一批属性赋值只会完整写入一次文件,不再每次赋值都写入;需要立即落盘时可显式调用 SaveProfile()

.NET 10 部分属性

目标项目为 .NET 10 或更高版本并使用 C# 14 时,可以直接声明由生成器实现的静态部分属性。生成器会保留属性初始值,并实现实例持久化存储、并发锁、集合绑定与自动保存:

[AutoLoadProfile]
partial class SystemProfile : XFEProfile
{
    [ProfileProperty]
    public static partial string Name { get; set; } = string.Empty;

    [ProfileProperty]
    public static partial int Age { get; set; }
}

SystemProfile.Name = "Test";
Console.WriteLine(SystemProfile.Age);

部分属性直接使用自然名称 Xxx,不需要 Instance 前缀,调用方式仍为 SystemProfile.Xxx,持久化名称同样为 Xxx。生成器会另外创建 InstanceXxx 作为序列化所需的实例桥接成员,但业务代码无需通过它访问配置。

旧的字段写法继续受到支持。对于目标为 .NET 10+ 且启用 C# 14 的项目,分析器会以 XFE0003 提示可升级字段,并提供“转换为 .NET 10 部分配置属性”代码修复。修复会保留初始值和 SystemProfile.Xxx 静态调用,更新字段引用及 get/set 特性中的字段名;原字段上的其他特性会改为 field: 目标。手动升级时可按以下方式转换:

// 旧写法
[ProfileProperty]
string name = "Guest";

// .NET 10 / C# 14 写法
[ProfileProperty]
public static partial string Name { get; set; } = "Guest";

详细用法

自动保存性能与并发访问

生成属性、ProfileList<T>ProfileDictionary<TKey, TValue> 均支持并发访问。每个配置实例只有一个写入器,自动保存请求会按照 AutoSaveDelay 合并;写入时先生成同目录临时文件,再原子替换目标文件,避免其他线程读取到写了一半的配置。

可以在配置类构造函数中调整合并窗口:

public SystemProfile()
{
    AutoSaveDelay = TimeSpan.FromMilliseconds(500);
}

Current.LastSaveException 可用于检查最近一次后台保存异常,成功保存后会自动清空。显式调用 SaveProfile() 仍是同步立即保存,写入异常会直接抛给调用方。SaveProfileAsync(CancellationToken) 用于强制异步保存,FlushAsync(CancellationToken) 用于等待已经请求的自动保存;应用退出前应调用其中之一,避免后台保存尚未完成。

已有配置加载失败时,原文件会移动为唯一的 .corrupt-* 备份,当前实例继续使用初始化默认值。可通过 Current.LastLoadException 或全局事件 XFEProfile.ProfileLoadFailed 获取失败详情。

修改存储格式

通过在实例构造函数中设置 DefaultProfileOperationMode 来更改存储格式,文件扩展名会自动更改:

[AutoLoadProfile]
partial class SystemProfile : XFEProfile
{
    [ProfileProperty]
    string name = string.Empty;

    [ProfileProperty]
    int _age;

    public SystemProfile()
    {
        DefaultProfileOperationMode = ProfileOperationMode.Xml; // 改用 XML 格式,扩展名自动变为 .xml
        // 可选值:ProfileOperationMode.XFEDictionary(默认)、Json、Xml、MessagePack、Custom
    }
}

配置版本、迁移与验证

重写 ProfileSchemaVersion,并在 ConfigureMigrations 中注册每个 N -> N + 1 步骤。没有版本元数据的旧文件视为版本 0。XFE 字典、JSON、XML、MessagePack 四种内置格式会自动保存版本;RenameProperty 同时处理所有内置格式。MessagePack 的自定义结构迁移使用 TransformMessagePack,未修改的属性会保持二进制形式,不会被反序列化。

XML 节点始终使用配置属性名称(例如 <Value>),不会包含实例桥接成员的 Instance 前缀。

[AutoLoadProfile]
partial class SystemProfile : XFEProfile
{
    [ProfileProperty("DisplayName")]
    string displayName = "访客";

    protected override int ProfileSchemaVersion => 2;

    protected override void ConfigureMigrations(ProfileMigrationBuilder migrations) => migrations
        .RenameProperty(0, "Name", "DisplayName")
        .Transform(1, context => UpgradeStructure(context.Content));

    protected override ProfileValidationResult ValidateProfile() =>
        string.IsNullOrWhiteSpace(InstanceDisplayName)
            ? ProfileValidationResult.Failure("DisplayName 不能为空")
            : ProfileValidationResult.Success;
}

加载过程是事务式的:先创建候选实例,再迁移、反序列化和验证,全部成功后才替换 Current。验证或迁移失败会记录在 LastLoadException 并触发 ProfileLoadFailed,原文件和当前实例均保持不变;成功迁移后会请求一次当前版本的自动保存。Custom 模式可重写 ReadCustomProfileVersionWriteCustomProfileVersion 定义自己的版本元数据。

JSON 选项和转换器

每个配置实例都有独立的 JsonOptions,JSON 模式和 XFE 字典中各属性值的 JSON 序列化都会使用它。可在构造函数中配置:

public SystemProfile()
{
    DefaultProfileOperationMode = ProfileOperationMode.Json;
    JsonOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
    JsonOptions.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase));
}

同一入口还支持自定义 converter、数字处理、大小写不敏感读取以及源生成的 JsonTypeInfoResolver

大型对象存储

大型嵌套对象可使用 MessagePack 二进制模式。默认的 contractless resolver 支持由公开属性组成的普通对象,无需添加 MessagePack 特性;文件扩展名为 .mpk

public SystemProfile()
{
    DefaultProfileOperationMode = ProfileOperationMode.MessagePack;

    // 可选:大型、重复数据较多时启用 LZ4 压缩
    MessagePackOptions = MessagePackOptions.WithCompression(
        MessagePack.MessagePackCompression.Lz4BlockArray);
}

文件保存和加载会直接处理二进制数据。大型配置需要在内存中导入或导出时,优先使用 ExportProfileBytes()ImportProfileBytes(ReadOnlyMemory<byte>),避免 Base64 转换;现有 ExportProfile()/ImportProfile(string) 在 MessagePack 模式下仍可用,字符串内容为 Base64。

多实例与多租户

生成的静态 API 仍适合单例配置。同一配置类型需要对应多个租户或文件时,使用 ProfileStore<TProfile>;传入的路径是包含扩展名的完整文件路径:

var tenantA = new ProfileStore<SystemProfile>("profiles/tenant-a.json");
var tenantB = new ProfileStore<SystemProfile>("profiles/tenant-b.json");

tenantA.Update(profile => profile.InstanceDisplayName = "租户 A");
tenantB.Update(profile => profile.InstanceDisplayName = "租户 B");
await Task.WhenAll(tenantA.SaveAsync(), tenantB.SaveAsync());

LoadSaveSaveAsyncFlushAsyncDeleteExportExportBytesImportImportBytes 只作用于对应存储;UpdateRead 会同步实例访问,集合也会自动绑定到正确实例。

自定义存储路径和文件扩展名

通过静态属性 ProfilePathProfileExtension 自定义存储位置:

[AutoLoadProfile]
partial class SystemProfile : XFEProfile
{
    [ProfileProperty]
    string name = string.Empty;

    [ProfileProperty]
    int _age;

    public SystemProfile()
    {
        ProfilePath = $"MyPath/MySubPath/{nameof(SystemProfile)}"; // 自定义路径(不含扩展名)
        ProfileExtension = ".ini";                                  // 自定义文件扩展名
    }
}

ProfilePathProfileExtension 均为框架自动生成的静态属性,也可在类外部直接赋值:

SystemProfile.ProfilePath = "custom/path/SystemProfile";
SystemProfile.ProfileExtension = ".cfg";

使用 [ProfilePath] 特性指定存储路径

[AutoLoadProfile]
[ProfilePath("MyPath/MySubPath/SystemProfile")]
partial class SystemProfile : XFEProfile
{
    [ProfileProperty]
    string name = string.Empty;

    [ProfileProperty]
    int _age;
}

自定义存储方法

DefaultProfileOperationMode 设为 Custom,并自行提供加载和保存方法:

[AutoLoadProfile]
partial class SystemProfile : XFEProfile
{
    [ProfileProperty]
    string name = string.Empty;

    [ProfileProperty]
    int _age;

    public SystemProfile()
    {
        DefaultProfileOperationMode = ProfileOperationMode.Custom;
        ProfilePath = $"MyPath/MySubPath/{nameof(SystemProfile)}";
        ProfileExtension = ".ini";
        LoadOperation = MyCustomLoadProfileOperation;
        SaveOperation = MyCustomSaveProfileOperation;
    }

    // 自定义加载方法
    public static XFEProfile? MyCustomLoadProfileOperation(XFEProfile profileInstance, string profileString, Dictionary<string, Type> propertyInfoDictionary, Dictionary<string, SetValueDelegate> propertySetFuncDictionary)
    {
        // 在此实现自定义加载逻辑
        return null;
    }

    // 自定义保存方法
    public static string MyCustomSaveProfileOperation(XFEProfile profileInstance, Dictionary<string, Type> propertyInfoDictionary, Dictionary<string, GetValueDelegate> propertyGetFuncDictionary)
    {
        // 在此实现自定义保存逻辑
        return string.Empty;
    }
}

使用 ProfileListProfileDictionary 存储集合

ProfileList<T>ProfileDictionary<TKey, TValue> 在集合发生变更(添加、删除、清空、索引赋值等操作)时会请求合并自动保存;其公开操作和快照枚举均可安全地并发使用:

生成器会在初始化、反序列化和重新赋值时自动绑定集合所属的配置实例,不再需要访问器代码特性或手工设置 CurrentProfile

[AutoLoadProfile]
partial class SystemProfile : XFEProfile
{
    [ProfileProperty]
    ProfileList<string> nameList = [];

    [ProfileProperty]
    ProfileDictionary<string, long> nameIdDictionary = [];
}

class Program
{
    static void Main(string[] args)
    {
        SystemProfile.NameList.Add("张三");               // 添加时自动保存
        SystemProfile.NameList.AddRange(["李四", "王五"]); // 批量添加
        SystemProfile.NameList.Remove("李四");            // 删除时也自动保存
        SystemProfile.NameIdDictionary.Add("张三", 100L); // 字典同理
    }
}

get/set 中插入自定义代码

使用 [ProfilePropertyAddGet][ProfilePropertyAddSet] 在生成的属性访问器中插入代码片段:

[AutoLoadProfile]
partial class SystemProfile : XFEProfile
{
    [ProfileProperty]
    [ProfilePropertyAddGet(@"Console.WriteLine(""获取了 Name"")")]
    [ProfilePropertyAddGet("return Current.name")]
    [ProfilePropertyAddSet(@"Console.WriteLine(""设置了 Name"")")]
    [ProfilePropertyAddSet("Current.name = value")]
    string name = string.Empty;

    [ProfileProperty]
    [ProfilePropertyAddGet(@"Console.WriteLine(""获取了 Age"")")]
    [ProfilePropertyAddGet("return Current._age")]
    [ProfilePropertyAddSet(@"Console.WriteLine(""设置了 Age"")")]
    [ProfilePropertyAddSet("Current._age = value")]
    int _age;
}

注意: 使用 [ProfilePropertyAddGet] / [ProfilePropertyAddSet] 时,需要自行完整处理返回值/赋值逻辑,最后一条 get 语句需包含 return

使用部分方法钩子

框架为每个属性自动生成 static partial void GetXxxProperty()static partial void SetXxxProperty(ref T value) 分部方法,可在用户代码中实现:

[AutoLoadProfile]
partial class SystemProfile : XFEProfile
{
    [ProfileProperty]
    string name = string.Empty;

    [ProfileProperty]
    int _age;

    static partial void GetNameProperty()
    {
        Console.WriteLine("获取了 Name");
    }

    static partial void SetNameProperty(ref string value)
    {
        Console.WriteLine($"设置了 Name:从 {Name} 变为 {value}");
    }

    static partial void GetAgeProperty()
    {
        Console.WriteLine("获取了 Age");
    }

    static partial void SetAgeProperty(ref int value)
    {
        value = 1999; // 可直接修改即将写入的值
        Console.WriteLine($"设置了 Age:从 {Age} 变为 1999");
    }
}

设置属性初始值

在字段声明处直接赋值即可:

[AutoLoadProfile]
partial class SystemProfile : XFEProfile
{
    [ProfileProperty]
    string name = "John Wick";

    [ProfileProperty]
    int _age = 59;
}

为字段添加 XML 文档注释

字段上的 <summary> 注释会被自动复制到生成的静态属性上:

[AutoLoadProfile]
partial class SystemProfile : XFEProfile
{
    /// <summary>
    /// 用户名称(此注释会自动同步至生成的 Name 属性)
    /// </summary>
    [ProfileProperty]
    string name = string.Empty;

    [ProfileProperty]
    int _age;
}

手动调用加载/保存/删除/导入/导出

框架为每个配置文件类自动生成以下静态方法:

SystemProfile.LoadProfile();                         // 从文件加载配置
SystemProfile.SaveProfile();                         // 将配置保存到文件
SystemProfile.DeleteProfile();                       // 删除配置文件
string exported = SystemProfile.ExportProfile();     // 将当前配置导出为字符串
SystemProfile.ImportProfile(exported);               // 从字符串导入配置

Native AOT 与裁剪策略

当前包不声明通用 Native AOT 或裁剪兼容(IsAotCompatible=falseIsTrimmable=false):

  • XML 模式使用基于反射的 XmlSerializer,不提供完整 Native AOT 保证。
  • 默认 JSON 和 XFE 字典操作使用运行时 Type 重载,并已标注 RequiresDynamicCode / RequiresUnreferencedCode。AOT 场景应把包含配置类型及全部属性类型的源生成 JsonSerializerContext 设置给 JsonOptions.TypeInfoResolver
  • 严格 AOT 部署应选择具备完整源生成元数据的 JSON,或在 Custom 模式中使用 AOT 安全的序列化器,并对实际应用执行 dotnet publish -p:PublishAot=true 验证。

这项策略避免示例或包元数据暗示所选序列化路径具备尚未实现的 AOT 保证。


API 参考

特性(Attributes)

特性 应用目标 说明
[ProfileProperty] 字段或部分属性 标记该成员参与自动生成,可指定属性名 [ProfileProperty("CustomName")]
[ProfilePropertyAddGet(code)] 字段或部分属性 在生成的 get 访问器中追加代码行,支持多个
[ProfilePropertyAddSet(code)] 字段或部分属性 在生成的 set 访问器中追加代码行,支持多个
[AutoLoadProfile(false)] 保持 Current 已初始化,但禁用自动文件加载
[ProfilePath(path)] 指定配置文件存储路径

存储模式(ProfileOperationMode)

文件扩展名 说明
XFEDictionary(默认) .xpf 使用 XFE 字典格式
Json .json 使用 JSON 序列化
Xml .xml 使用 XML 序列化
MessagePack .mpk 二进制序列化,适合大型对象
Custom 自定义 使用自定义的加载/保存委托

自动生成的静态成员

对于每个继承 XFEProfile 并使用 [ProfileProperty]partial 类,框架将自动生成:

成员 类型 说明
Current static T 当前配置文件实例
ProfilePath static string 配置文件路径(不含扩展名)
ProfileExtension static string 配置文件扩展名(空则自动推断)
LoadProfile() static void 从文件加载配置
SaveProfile() static void 立即保存配置并等待写入完成
SaveProfileAsync(CancellationToken) static Task 立即异步保存配置
FlushAsync(CancellationToken) static Task 写入已经请求的自动保存
DeleteProfile() static void 删除配置文件
ExportProfile() static string 导出配置为字符串
ExportProfileBytes() static byte[] 直接导出配置字节,适合 MessagePack
ImportProfile(string) static void 从字符串导入配置
ImportProfileBytes(ReadOnlyMemory<byte>) static void 直接从配置字节导入
Xxx(字段或部分属性模式) static T 配置的静态访问入口,读写时自动持久化
InstanceXxx T(实例) 生成器提供给序列化器的实例桥接属性
GetXxxProperty() static partial void get 钩子分部方法
SetXxxProperty(ref T) static partial void set 钩子分部方法

XFEProfile 基类成员

成员 类型 说明
DefaultProfileOperationMode ProfileOperationMode 存储/加载模式
LoadOperation ProfileLoadOperation 自定义加载委托
SaveOperation ProfileSaveOperation 自定义保存委托
ProfilesDefaultPath static string 所有配置文件的默认根目录
AutoSaveDelay TimeSpan 自动保存合并窗口(默认 100ms)
LastSaveException Exception? 最近一次后台保存异常
LastLoadException Exception? 当前配置最近一次加载异常
ProfileLoadFailed static event 加载失败并尝试保留损坏文件后触发
ProfileSchemaVersion protected int 写入内置格式的当前结构版本
LoadedProfileVersion int 最近成功加载/导入时的源版本
JsonOptions JsonSerializerOptions 每实例 JSON 命名、converter 和类型元数据选项
MessagePackOptions MessagePackSerializerOptions 每实例 MessagePack resolver、安全和压缩选项
ConfigureMigrations(...) 虚方法钩子 注册连续的结构迁移步骤
ValidateProfile() 虚方法钩子 候选配置替换当前实例前执行验证

许可证

本项目基于 MIT 许可证 开源。