XFEExtension.NetCore.AutoConfig
📖 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 模式可重写 ReadCustomProfileVersion 和 WriteCustomProfileVersion 定义自己的版本元数据。
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());
Load、Save、SaveAsync、FlushAsync、Delete、Export、ExportBytes、Import、ImportBytes 只作用于对应存储;Update 和 Read 会同步实例访问,集合也会自动绑定到正确实例。
自定义存储路径和文件扩展名
通过静态属性 ProfilePath 和 ProfileExtension 自定义存储位置:
[AutoLoadProfile]
partial class SystemProfile : XFEProfile
{
[ProfileProperty]
string name = string.Empty;
[ProfileProperty]
int _age;
public SystemProfile()
{
ProfilePath = $"MyPath/MySubPath/{nameof(SystemProfile)}"; // 自定义路径(不含扩展名)
ProfileExtension = ".ini"; // 自定义文件扩展名
}
}
ProfilePath和ProfileExtension均为框架自动生成的静态属性,也可在类外部直接赋值: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;
}
}
使用 ProfileList 和 ProfileDictionary 存储集合
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=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<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 许可证 开源。