# XFEExtension.NetCore.AutoConfig [![NuGet](https://img.shields.io/nuget/v/XFEExtension.NetCore.AutoConfig?label=NuGet&logo=NuGet)](https://www.nuget.org/packages/XFEExtension.NetCore.AutoConfig/) [![NuGet Downloads](https://img.shields.io/nuget/dt/XFEExtension.NetCore.AutoConfig?label=Downloads&logo=NuGet)](https://www.nuget.org/packages/XFEExtension.NetCore.AutoConfig/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE.txt) [![.NET](https://img.shields.io/badge/.NET-10.0-512BD4)](https://dotnet.microsoft.com/download) > 📖 [English](https://github.com/XFEstudio/XFEExtension.NetCore.AutoConfig/blob/master/README.md) | 简体中文 ## 描述 XFEExtension.NetCore.AutoConfig 是一个基于 Roslyn 增量源生成器的 .NET 库,可以自动为继承自 `XFEProfile` 的配置文件类生成属性、加载/保存方法,实现配置文件的自动持久化存储。 ## 快速开始 ### 安装 ```shell dotnet add package XFEExtension.NetCore.AutoConfig ``` ### 基础用法 为字段添加 `[ProfileProperty]` 特性,框架将自动生成对应的静态属性,并在赋值时自动保存配置: ```csharp // 创建配置文件类 [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 时,可以直接声明由生成器实现的静态部分属性。生成器会保留属性初始值,并实现实例持久化存储、并发锁、集合绑定与自动保存: ```csharp [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:` 目标。手动升级时可按以下方式转换: ```csharp // 旧写法 [ProfileProperty] string name = "Guest"; // .NET 10 / C# 14 写法 [ProfileProperty] public static partial string Name { get; set; } = "Guest"; ``` --- ## 详细用法 ### 自动保存性能与并发访问 生成属性、`ProfileList` 和 `ProfileDictionary` 均支持并发访问。每个配置实例只有一个写入器,自动保存请求会按照 `AutoSaveDelay` 合并;写入时先生成同目录临时文件,再原子替换目标文件,避免其他线程读取到写了一半的配置。 可以在配置类构造函数中调整合并窗口: ```csharp public SystemProfile() { AutoSaveDelay = TimeSpan.FromMilliseconds(500); } ``` `Current.LastSaveException` 可用于检查最近一次后台保存异常,成功保存后会自动清空。显式调用 `SaveProfile()` 仍是同步立即保存,写入异常会直接抛给调用方。`SaveProfileAsync(CancellationToken)` 用于强制异步保存,`FlushAsync(CancellationToken)` 用于等待已经请求的自动保存;应用退出前应调用其中之一,避免后台保存尚未完成。 已有配置加载失败时,原文件会移动为唯一的 `.corrupt-*` 备份,当前实例继续使用初始化默认值。可通过 `Current.LastLoadException` 或全局事件 `XFEProfile.ProfileLoadFailed` 获取失败详情。 ### 修改存储格式 通过在实例构造函数中设置 `DefaultProfileOperationMode` 来更改存储格式,文件扩展名会自动更改: ```csharp [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 节点始终使用配置属性名称(例如 ``),不会包含实例桥接成员的 `Instance` 前缀。 ```csharp [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` 模式可重写 `ReadCustomProfileVersion` 和 `WriteCustomProfileVersion` 定义自己的版本元数据。 ### JSON 选项和转换器 每个配置实例都有独立的 `JsonOptions`,JSON 模式和 XFE 字典中各属性值的 JSON 序列化都会使用它。可在构造函数中配置: ```csharp public SystemProfile() { DefaultProfileOperationMode = ProfileOperationMode.Json; JsonOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; JsonOptions.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)); } ``` 同一入口还支持自定义 converter、数字处理、大小写不敏感读取以及源生成的 `JsonTypeInfoResolver`。 ### 大型对象存储 大型嵌套对象可使用 MessagePack 二进制模式。默认的 contractless resolver 支持由公开属性组成的普通对象,无需添加 MessagePack 特性;文件扩展名为 `.mpk`: ```csharp public SystemProfile() { DefaultProfileOperationMode = ProfileOperationMode.MessagePack; // 可选:大型、重复数据较多时启用 LZ4 压缩 MessagePackOptions = MessagePackOptions.WithCompression( MessagePack.MessagePackCompression.Lz4BlockArray); } ``` 文件保存和加载会直接处理二进制数据。大型配置需要在内存中导入或导出时,优先使用 `ExportProfileBytes()` 和 `ImportProfileBytes(ReadOnlyMemory)`,避免 Base64 转换;现有 `ExportProfile()`/`ImportProfile(string)` 在 MessagePack 模式下仍可用,字符串内容为 Base64。 ### 多实例与多租户 生成的静态 API 仍适合单例配置。同一配置类型需要对应多个租户或文件时,使用 `ProfileStore`;传入的路径是包含扩展名的完整文件路径: ```csharp var tenantA = new ProfileStore("profiles/tenant-a.json"); var tenantB = new ProfileStore("profiles/tenant-b.json"); tenantA.Update(profile => profile.InstanceDisplayName = "租户 A"); tenantB.Update(profile => profile.InstanceDisplayName = "租户 B"); await Task.WhenAll(tenantA.SaveAsync(), tenantB.SaveAsync()); ``` `Load`、`Save`、`SaveAsync`、`FlushAsync`、`Delete`、`Export`、`ExportBytes`、`Import`、`ImportBytes` 只作用于对应存储;`Update` 和 `Read` 会同步实例访问,集合也会自动绑定到正确实例。 ### 自定义存储路径和文件扩展名 通过静态属性 `ProfilePath` 和 `ProfileExtension` 自定义存储位置: ```csharp [AutoLoadProfile] partial class SystemProfile : XFEProfile { [ProfileProperty] string name = string.Empty; [ProfileProperty] int _age; public SystemProfile() { ProfilePath = $"MyPath/MySubPath/{nameof(SystemProfile)}"; // 自定义路径(不含扩展名) ProfileExtension = ".ini"; // 自定义文件扩展名 } } ``` > `ProfilePath` 和 `ProfileExtension` 均为框架自动生成的静态属性,也可在类外部直接赋值: > ```csharp > SystemProfile.ProfilePath = "custom/path/SystemProfile"; > SystemProfile.ProfileExtension = ".cfg"; > ``` ### 使用 `[ProfilePath]` 特性指定存储路径 ```csharp [AutoLoadProfile] [ProfilePath("MyPath/MySubPath/SystemProfile")] partial class SystemProfile : XFEProfile { [ProfileProperty] string name = string.Empty; [ProfileProperty] int _age; } ``` ### 自定义存储方法 将 `DefaultProfileOperationMode` 设为 `Custom`,并自行提供加载和保存方法: ```csharp [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 propertyInfoDictionary, Dictionary propertySetFuncDictionary) { // 在此实现自定义加载逻辑 return null; } // 自定义保存方法 public static string MyCustomSaveProfileOperation(XFEProfile profileInstance, Dictionary propertyInfoDictionary, Dictionary propertyGetFuncDictionary) { // 在此实现自定义保存逻辑 return string.Empty; } } ``` ### 使用 `ProfileList` 和 `ProfileDictionary` 存储集合 `ProfileList` 和 `ProfileDictionary` 在集合发生变更(添加、删除、清空、索引赋值等操作)时会请求合并自动保存;其公开操作和快照枚举均可安全地并发使用: 生成器会在初始化、反序列化和重新赋值时自动绑定集合所属的配置实例,不再需要访问器代码特性或手工设置 `CurrentProfile`。 ```csharp [AutoLoadProfile] partial class SystemProfile : XFEProfile { [ProfileProperty] ProfileList nameList = []; [ProfileProperty] ProfileDictionary 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]` 在生成的属性访问器中插入代码片段: ```csharp [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)` 分部方法,可在用户代码中实现: ```csharp [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"); } } ``` ### 设置属性初始值 在字段声明处直接赋值即可: ```csharp [AutoLoadProfile] partial class SystemProfile : XFEProfile { [ProfileProperty] string name = "John Wick"; [ProfileProperty] int _age = 59; } ``` ### 为字段添加 XML 文档注释 字段上的 `` 注释会被自动复制到生成的静态属性上: ```csharp [AutoLoadProfile] partial class SystemProfile : XFEProfile { /// /// 用户名称(此注释会自动同步至生成的 Name 属性) /// [ProfileProperty] string name = string.Empty; [ProfileProperty] int _age; } ``` ### 手动调用加载/保存/删除/导入/导出 框架为每个配置文件类自动生成以下静态方法: ```csharp SystemProfile.LoadProfile(); // 从文件加载配置 SystemProfile.SaveProfile(); // 将配置保存到文件 SystemProfile.DeleteProfile(); // 删除配置文件 string exported = SystemProfile.ExportProfile(); // 将当前配置导出为字符串 SystemProfile.ImportProfile(exported); // 从字符串导入配置 ``` ### Native AOT 与裁剪策略 当前包**不声明**通用 Native AOT 或裁剪兼容(`IsAotCompatible=false`、`IsTrimmable=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)` | `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 许可证](LICENSE.txt) 开源。