返回提交历史
Modified
AutoConfig.Analyzer.Test/AutoConfig.Analyzer.Test.csproj
+1
-1
Modified
README.md
+24
-3
Modified
README.zh-CN.md
+24
-3
Modified
XFEExtension.NetCore.AutoConfig.Analyzer/Generator/ProfilePropertyAutoGenerator.cs
+12
-1
Modified
XFEExtension.NetCore.AutoConfig.Tests/GeneratorAndCodeFixTests.cs
+3
-1
Added
XFEExtension.NetCore.AutoConfig.Tests/MessagePackPersistenceTests.cs
+106
-0
Modified
XFEExtension.NetCore.AutoConfig.Tests/PersistenceProfiles.cs
+54
-0
Modified
XFEExtension.NetCore.AutoConfig.Tests/XFEExtension.NetCore.AutoConfig.Tests.csproj
+2
-2
Added
XFEExtension.NetCore.AutoConfig/MessagePackProfileSerializer.cs
+61
-0
Modified
XFEExtension.NetCore.AutoConfig/ProfileMigration.cs
+122
-3
Modified
XFEExtension.NetCore.AutoConfig/ProfileOperationMode.cs
+5
-1
Modified
XFEExtension.NetCore.AutoConfig/ProfileStore.cs
+14
-0
Modified
XFEExtension.NetCore.AutoConfig/XFEExtension.NetCore.AutoConfig.csproj
+2
-1
Modified
XFEExtension.NetCore.AutoConfig/XFEProfile.cs
+89
-18
XFEstudio/XFEExtension.NetCore.AutoConfig
支持 MessagePack 二进制序列化配置文件
新增对 MessagePack 格式的全面支持,适用于大型对象配置,扩展名为 .mpk,`ProfileOperationMode` 增加 MessagePack 选项。`XFEProfile` 增加 MessagePackOptions 属性,默认启用 contractless resolver。配置文件的保存、加载、导入、导出等流程支持 MessagePack,底层采用原始字节读写,避免 Base64。新增导入/导出二进制静态方法,迁移系统支持 MessagePack,属性值保持二进制,迁移方法自动兼容。新增 MessagePackProfileSerializer 辅助类及大对象配置类型和相关单元测试。文档补充 MessagePack 使用说明和 API 变更,代码生成器和测试用例同步更新,升级依赖并引入 MessagePack。
99ef9f2
代码差异
14 个文件
+519
-34
@@ -9,7 +9,7 @@
9
9
</PropertyGroup>
10
10
11
11
<ItemGroup>
12
<PackageReference Include="XFEExtension.NetCore" Version="4.2.3" />
12
<PackageReference Include="XFEExtension.NetCore" Version="5.2.0" />
13
13
</ItemGroup>
14
14
15
15
<ItemGroup>
@@ -89,14 +89,14 @@ partial class SystemProfile : XFEProfile
89
89
public SystemProfile()
90
90
{
91
91
DefaultProfileOperationMode = ProfileOperationMode.Xml; // Switch to XML; extension becomes .xml
92
// Available modes: XFEDictionary (default), Json, Xml, Custom
92
// Available modes: XFEDictionary (default), Json, Xml, MessagePack, Custom
93
93
}
94
94
}
95
95
```
96
96
97
97
### Schema Versions, Migrations, and Validation
98
98
99
Override `ProfileSchemaVersion` and register every `N -> N + 1` step in `ConfigureMigrations`. Files without metadata are version `0`. Built-in XFE dictionary, JSON, and XML modes persist version metadata automatically; `RenameProperty` handles XFE dictionary keys, generated `InstanceXxx` JSON members, and XML element names.
99
Override `ProfileSchemaVersion` and register every `N -> N + 1` step in `ConfigureMigrations`. Files without metadata are version `0`. Built-in XFE dictionary, JSON, XML, and MessagePack modes persist version metadata automatically, and `RenameProperty` handles every built-in format. Use `TransformMessagePack` for custom MessagePack migrations; untouched properties remain binary and are not deserialized during migration.
100
100
101
101
XML element names use the profile property name (for example, `<Value>`) even though the generated C# instance property remains `InstanceValue`.
102
102
@@ -137,6 +137,23 @@ public SystemProfile()
137
137
138
138
The same options support custom converters, number handling, case-insensitive reads, and a source-generated `JsonTypeInfoResolver`.
139
139
140
### Large Object Storage
141
142
Use the MessagePack binary mode for large nested objects. Its default contractless resolver supports ordinary objects with public properties without requiring MessagePack attributes, and files use the `.mpk` extension:
143
144
```csharp
145
public SystemProfile()
146
{
147
DefaultProfileOperationMode = ProfileOperationMode.MessagePack;
148
149
// Optional for large data with substantial repetition
150
MessagePackOptions = MessagePackOptions.WithCompression(
151
MessagePack.MessagePackCompression.Lz4BlockArray);
152
}
153
```
154
155
File save/load stays binary throughout. For in-memory transfer of a large profile, prefer `ExportProfileBytes()` and `ImportProfileBytes(ReadOnlyMemory<byte>)` to avoid Base64 conversion. Existing `ExportProfile()` and `ImportProfile(string)` remain available in MessagePack mode and use Base64 strings.
156
140
157
### Multiple Instances and Tenants
141
158
142
159
The generated static API remains the simplest singleton API. Use `ProfileStore<TProfile>` when one configuration type needs independent files or tenants. Its path is a complete file path including the extension:
@@ -150,7 +167,7 @@ tenantB.Update(profile => profile.InstanceDisplayName = "Tenant B");
150
167
await Task.WhenAll(tenantA.SaveAsync(), tenantB.SaveAsync());
151
168
```
152
169
153
`Load`, `Save`, `SaveAsync`, `FlushAsync`, `Delete`, `Export`, and `Import` operate only on that store. `Update` and `Read` synchronize instance access, and collections are automatically bound to the correct owning instance.
170
`Load`, `Save`, `SaveAsync`, `FlushAsync`, `Delete`, `Export`, `ExportBytes`, `Import`, and `ImportBytes` operate only on that store. `Update` and `Read` synchronize instance access, and collections are automatically bound to the correct owning instance.
154
171
155
172
### Custom Storage Path and File Extension
156
173
@@ -411,6 +428,7 @@ This explicit policy prevents the sample or package metadata from implying an AO
411
428
| `XFEDictionary` (default) | `.xpf` | XFE dictionary format |
412
429
| `Json` | `.json` | JSON serialization |
413
430
| `Xml` | `.xml` | XML serialization |
431
| `MessagePack` | `.mpk` | Binary serialization for large objects |
414
432
| `Custom` | custom | User-provided load/save delegates |
415
433
416
434
### Auto-Generated Static Members
@@ -428,7 +446,9 @@ For every `partial` class that inherits `XFEProfile` and uses `[ProfileProperty]
428
446
| `FlushAsync(CancellationToken)` | `static Task` | Flushes an already requested automatic save |
429
447
| `DeleteProfile()` | `static void` | Deletes the config file |
430
448
| `ExportProfile()` | `static string` | Exports config as a string |
449
| `ExportProfileBytes()` | `static byte[]` | Exports raw config bytes; preferred for MessagePack |
431
450
| `ImportProfile(string)` | `static void` | Imports config from a string |
451
| `ImportProfileBytes(ReadOnlyMemory<byte>)` | `static void` | Imports config directly from bytes |
432
452
| `XxxProperty` (per field) | `static T` | Auto-generated static property; saves on set |
433
453
| `InstanceXxx` (per field) | `T` (instance) | Corresponding instance property |
434
454
| `GetXxxProperty()` | `static partial void` | Invoked when the property is read |
@@ -449,6 +469,7 @@ For every `partial` class that inherits `XFEProfile` and uses `[ProfileProperty]
449
469
| `ProfileSchemaVersion` | protected `int` | Current schema version written to built-in formats |
450
470
| `LoadedProfileVersion` | `int` | Source version from the last successful load/import |
451
471
| `JsonOptions` | `JsonSerializerOptions` | Per-instance JSON naming, converter, and metadata options |
472
| `MessagePackOptions` | `MessagePackSerializerOptions` | Per-instance MessagePack resolver, security, and compression options |
452
473
| `ConfigureMigrations(...)` | virtual hook | Registers sequential schema migrations |
453
474
| `ValidateProfile()` | virtual hook | Validates a candidate before it replaces the current profile |
454
475
@@ -89,14 +89,14 @@ partial class SystemProfile : XFEProfile
89
89
public SystemProfile()
90
90
{
91
91
DefaultProfileOperationMode = ProfileOperationMode.Xml; // 改用 XML 格式,扩展名自动变为 .xml
92
// 可选值:ProfileOperationMode.XFEDictionary(默认)、Json、Xml、Custom
92
// 可选值:ProfileOperationMode.XFEDictionary(默认)、Json、Xml、MessagePack、Custom
93
93
}
94
94
}
95
95
```
96
96
97
97
### 配置版本、迁移与验证
98
98
99
重写 `ProfileSchemaVersion`,并在 `ConfigureMigrations` 中注册每个 `N -> N + 1` 步骤。没有版本元数据的旧文件视为版本 `0`。XFE 字典、JSON、XML 三种内置格式会自动保存版本;`RenameProperty` 同时处理 XFE 字典键、JSON 中生成的 `InstanceXxx` 成员以及 XML 节点名称。
99
重写 `ProfileSchemaVersion`,并在 `ConfigureMigrations` 中注册每个 `N -> N + 1` 步骤。没有版本元数据的旧文件视为版本 `0`。XFE 字典、JSON、XML、MessagePack 四种内置格式会自动保存版本;`RenameProperty` 同时处理所有内置格式。MessagePack 的自定义结构迁移使用 `TransformMessagePack`,未修改的属性会保持二进制形式,不会被反序列化。
100
100
101
101
XML 节点会使用配置属性名称(例如 `<Value>`),生成的 C# 实例属性仍保持为 `InstanceValue`。
102
102
@@ -137,6 +137,23 @@ public SystemProfile()
137
137
138
138
同一入口还支持自定义 converter、数字处理、大小写不敏感读取以及源生成的 `JsonTypeInfoResolver`。
139
139
140
### 大型对象存储
141
142
大型嵌套对象可使用 MessagePack 二进制模式。默认的 contractless resolver 支持由公开属性组成的普通对象,无需添加 MessagePack 特性;文件扩展名为 `.mpk`:
143
144
```csharp
145
public SystemProfile()
146
{
147
DefaultProfileOperationMode = ProfileOperationMode.MessagePack;
148
149
// 可选:大型、重复数据较多时启用 LZ4 压缩
150
MessagePackOptions = MessagePackOptions.WithCompression(
151
MessagePack.MessagePackCompression.Lz4BlockArray);
152
}
153
```
154
155
文件保存和加载会直接处理二进制数据。大型配置需要在内存中导入或导出时,优先使用 `ExportProfileBytes()` 和 `ImportProfileBytes(ReadOnlyMemory<byte>)`,避免 Base64 转换;现有 `ExportProfile()`/`ImportProfile(string)` 在 MessagePack 模式下仍可用,字符串内容为 Base64。
156
140
157
### 多实例与多租户
141
158
142
159
生成的静态 API 仍适合单例配置。同一配置类型需要对应多个租户或文件时,使用 `ProfileStore<TProfile>`;传入的路径是包含扩展名的完整文件路径:
@@ -150,7 +167,7 @@ tenantB.Update(profile => profile.InstanceDisplayName = "租户 B");
150
167
await Task.WhenAll(tenantA.SaveAsync(), tenantB.SaveAsync());
151
168
```
152
169
153
`Load`、`Save`、`SaveAsync`、`FlushAsync`、`Delete`、`Export`、`Import` 只作用于对应存储;`Update` 和 `Read` 会同步实例访问,集合也会自动绑定到正确实例。
170
`Load`、`Save`、`SaveAsync`、`FlushAsync`、`Delete`、`Export`、`ExportBytes`、`Import`、`ImportBytes` 只作用于对应存储;`Update` 和 `Read` 会同步实例访问,集合也会自动绑定到正确实例。
154
171
155
172
### 自定义存储路径和文件扩展名
156
173
@@ -404,6 +421,7 @@ SystemProfile.ImportProfile(exported); // 从字符串导入配置
404
421
| `XFEDictionary`(默认)| `.xpf` | 使用 XFE 字典格式 |
405
422
| `Json` | `.json` | 使用 JSON 序列化 |
406
423
| `Xml` | `.xml` | 使用 XML 序列化 |
424
| `MessagePack` | `.mpk` | 二进制序列化,适合大型对象 |
407
425
| `Custom` | 自定义 | 使用自定义的加载/保存委托 |
408
426
409
427
### 自动生成的静态成员
@@ -421,7 +439,9 @@ SystemProfile.ImportProfile(exported); // 从字符串导入配置
421
439
| `FlushAsync(CancellationToken)` | `static Task` | 写入已经请求的自动保存 |
422
440
| `DeleteProfile()` | `static void` | 删除配置文件 |
423
441
| `ExportProfile()` | `static string` | 导出配置为字符串 |
442
| `ExportProfileBytes()` | `static byte[]` | 直接导出配置字节,适合 MessagePack |
424
443
| `ImportProfile(string)` | `static void` | 从字符串导入配置 |
444
| `ImportProfileBytes(ReadOnlyMemory<byte>)` | `static void` | 直接从配置字节导入 |
425
445
| `XxxProperty`(每个字段)| `static T` | 自动生成的静态属性,读写时自动持久化 |
426
446
| `InstanceXxx`(每个字段)| `T`(实例)| 对应的实例属性 |
427
447
| `GetXxxProperty()` | `static partial void` | get 钩子分部方法 |
@@ -442,6 +462,7 @@ SystemProfile.ImportProfile(exported); // 从字符串导入配置
442
462
| `ProfileSchemaVersion` | protected `int` | 写入内置格式的当前结构版本 |
443
463
| `LoadedProfileVersion` | `int` | 最近成功加载/导入时的源版本 |
444
464
| `JsonOptions` | `JsonSerializerOptions` | 每实例 JSON 命名、converter 和类型元数据选项 |
465
| `MessagePackOptions` | `MessagePackSerializerOptions` | 每实例 MessagePack resolver、安全和压缩选项 |
445
466
| `ConfigureMigrations(...)` | 虚方法钩子 | 注册连续的结构迁移步骤 |
446
467
| `ValidateProfile()` | 虚方法钩子 | 候选配置替换当前实例前执行验证 |
447
468
@@ -140,7 +140,7 @@ public sealed class ProfilePropertyAutoGenerator : IIncrementalGenerator
140
140
{
141
141
"__profileInstanceSyncRoot", "__current", "__profilePath", "__profileExtension", "__UpdateCurrentProfilePath", "__BindProfileOwnedCollections",
142
142
"Current", "ProfilePath", "ProfileExtension", "Initialize", "LoadProfile", "SaveProfile", "SaveProfileAsync",
143
"FlushAsync", "DeleteProfile", "ExportProfile", "ImportProfile"
143
"FlushAsync", "DeleteProfile", "ExportProfile", "ExportProfileBytes", "ImportProfile", "ImportProfileBytes"
144
144
};
145
145
foreach (var reservedMember in reservedMembers)
146
146
{
@@ -371,6 +371,11 @@ public sealed class ProfilePropertyAutoGenerator : IIncrementalGenerator
371
371
builder.AppendLine(" lock (__profileInstanceSyncRoot) return __current.InstanceExportProfile();");
372
372
builder.AppendLine(" }");
373
373
builder.AppendLine();
374
builder.AppendLine(" public static byte[] ExportProfileBytes()");
375
builder.AppendLine(" {");
376
builder.AppendLine(" lock (__profileInstanceSyncRoot) return __current.InstanceExportProfileBytes();");
377
builder.AppendLine(" }");
378
builder.AppendLine();
374
379
builder.AppendLine(" public static void ImportProfile(string profileString)");
375
380
builder.AppendLine(" {");
376
381
builder.AppendLine(" if (profileString is null) throw new global::System.ArgumentNullException(nameof(profileString));");
@@ -378,6 +383,12 @@ public sealed class ProfilePropertyAutoGenerator : IIncrementalGenerator
378
383
builder.AppendLine(" if (__current.InstanceImportProfile(profileString, static () => new " + typeName + "()) is " + typeName + " importedProfile) __current = importedProfile;");
379
384
builder.AppendLine(" }");
380
385
builder.AppendLine();
386
builder.AppendLine(" public static void ImportProfileBytes(global::System.ReadOnlyMemory<byte> profileContent)");
387
builder.AppendLine(" {");
388
builder.AppendLine(" lock (__profileInstanceSyncRoot)");
389
builder.AppendLine(" if (__current.InstanceImportProfileBytes(profileContent, static () => new " + typeName + "()) is " + typeName + " importedProfile) __current = importedProfile;");
390
builder.AppendLine(" }");
391
builder.AppendLine();
381
392
}
382
393
383
394
private static void AppendFieldMembers(StringBuilder builder, FieldModel field)
@@ -66,7 +66,9 @@ public sealed class GeneratorAndCodeFixTests
66
66
var hash = Convert.ToHexString(System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(generatedSource)));
67
67
68
68
Assert.Contains("[global::System.Xml.Serialization.XmlElementAttribute(\"Value\")]", generatedSource);
69
Assert.Equal("BFCE10C01C4BBD8D156D2AFB85A55DC4B545B292AFF2841197BE70599FD3FBA5", hash);
69
Assert.Contains("public static byte[] ExportProfileBytes()", generatedSource);
70
Assert.Contains("public static void ImportProfileBytes(global::System.ReadOnlyMemory<byte> profileContent)", generatedSource);
71
Assert.Equal("7B0B7B07FE1271B592876778731820ACB41520A9F836C6D3F7501735F9A839A8", hash);
70
72
}
71
73
72
74
[Theory]
@@ -0,0 +1,106 @@
1
using Xunit;
2
3
namespace XFEExtension.NetCore.AutoConfig.Tests;
4
5
public sealed class MessagePackPersistenceTests
6
{
7
[Fact]
8
public async Task LargeObjectRoundTripsThroughBinaryFile()
9
{
10
var directory = CreateTestDirectory();
11
try
12
{
13
LargeObjectProfile.Current = new LargeObjectProfile();
14
LargeObjectProfile.ProfilePath = Path.Combine(directory, nameof(LargeObjectProfile));
15
var source = CreateLargeDocument(12_000);
16
LargeObjectProfile.Current.InstanceData = source;
17
18
await LargeObjectProfile.SaveProfileAsync();
19
20
var profilePath = LargeObjectProfile.ProfilePath + ".mpk";
21
Assert.True(File.Exists(profilePath));
22
var persisted = await File.ReadAllBytesAsync(profilePath);
23
Assert.Equal(LargeObjectProfile.ExportProfileBytes(), persisted);
24
25
LargeObjectProfile.Current.InstanceData = new LargeSettingsDocument { Name = "changed" };
26
LargeObjectProfile.LoadProfile();
27
28
Assert.Equal(source.Name, LargeObjectProfile.Data.Name);
29
Assert.Equal(source.Metadata, LargeObjectProfile.Data.Metadata);
30
Assert.Equal(source.Items.Count, LargeObjectProfile.Data.Items.Count);
31
Assert.Equal(source.Items[7_777].Samples, LargeObjectProfile.Data.Items[7_777].Samples);
32
Assert.Equal(1, LargeObjectProfile.Current.LoadedProfileVersion);
33
}
34
finally
35
{
36
LargeObjectProfile.DeleteProfile();
37
DeleteDirectory(directory);
38
}
39
}
40
41
[Fact]
42
public void ByteAndBase64ImportExportRoundTripWithoutStreams()
43
{
44
var source = CreateLargeDocument(256);
45
LargeObjectProfile.Current = new LargeObjectProfile { InstanceData = source };
46
47
var bytes = LargeObjectProfile.ExportProfileBytes();
48
LargeObjectProfile.Current.InstanceData = new LargeSettingsDocument();
49
LargeObjectProfile.ImportProfileBytes(bytes);
50
Assert.Equal(source.Items.Count, LargeObjectProfile.Data.Items.Count);
51
52
var base64 = LargeObjectProfile.ExportProfile();
53
Assert.Equal(bytes, Convert.FromBase64String(base64));
54
LargeObjectProfile.Current.InstanceData = new LargeSettingsDocument();
55
LargeObjectProfile.ImportProfile(base64);
56
Assert.Equal(source.Items[128].Name, LargeObjectProfile.Data.Items[128].Name);
57
}
58
59
[Fact]
60
public void MessagePackMigrationRenamesLargeObjectProperty()
61
{
62
var source = CreateLargeDocument(64);
63
UnversionedLargeObjectProfile.Current = new UnversionedLargeObjectProfile { InstanceData = source };
64
var versionZero = UnversionedLargeObjectProfile.ExportProfileBytes();
65
66
MigratingLargeObjectProfile.Current = new MigratingLargeObjectProfile();
67
MigratingLargeObjectProfile.ImportProfileBytes(versionZero);
68
69
Assert.Equal(0, MigratingLargeObjectProfile.Current.LoadedProfileVersion);
70
Assert.Equal(source.Name, MigratingLargeObjectProfile.CurrentData.Name);
71
Assert.Equal(source.Items[42].Samples, MigratingLargeObjectProfile.CurrentData.Items[42].Samples);
72
}
73
74
private static LargeSettingsDocument CreateLargeDocument(int count)
75
{
76
var document = new LargeSettingsDocument
77
{
78
Name = "large-settings",
79
Metadata = Enumerable.Range(0, 64).ToDictionary(index => $"key-{index}", index => $"value-{index}"),
80
Items = new List<LargeSettingsItem>(count)
81
};
82
for (var index = 0; index < count; index++)
83
{
84
document.Items.Add(new LargeSettingsItem
85
{
86
Id = index,
87
Name = $"item-{index}",
88
Samples = [index * 0.25, index * 0.5, index * 0.75, index]
89
});
90
}
91
return document;
92
}
93
94
private static string CreateTestDirectory()
95
{
96
var directory = Path.Combine(Path.GetTempPath(), "XFEAutoConfigTests", Guid.NewGuid().ToString("N"));
97
Directory.CreateDirectory(directory);
98
return directory;
99
}
100
101
private static void DeleteDirectory(string directory)
102
{
103
if (Directory.Exists(directory))
104
Directory.Delete(directory, true);
105
}
106
}
@@ -69,3 +69,57 @@ public partial class PathChangeProfile : XFEProfile
69
69
[ProfileProperty]
70
70
private int value;
71
71
}
72
73
public sealed class LargeSettingsDocument
74
{
75
public string Name { get; set; } = string.Empty;
76
77
public Dictionary<string, string> Metadata { get; set; } = [];
78
79
public List<LargeSettingsItem> Items { get; set; } = [];
80
}
81
82
public sealed class LargeSettingsItem
83
{
84
public int Id { get; set; }
85
86
public string Name { get; set; } = string.Empty;
87
88
public double[] Samples { get; set; } = [];
89
}
90
91
[AutoLoadProfile(false)]
92
public partial class LargeObjectProfile : XFEProfile
93
{
94
[ProfileProperty("Data")]
95
private LargeSettingsDocument data = new();
96
97
public LargeObjectProfile() => DefaultProfileOperationMode = ProfileOperationMode.MessagePack;
98
99
protected override int ProfileSchemaVersion => 1;
100
101
protected override void ConfigureMigrations(ProfileMigrationBuilder migrations) => migrations.NoOp(0);
102
}
103
104
[AutoLoadProfile(false)]
105
public partial class UnversionedLargeObjectProfile : XFEProfile
106
{
107
[ProfileProperty("Data")]
108
private LargeSettingsDocument data = new();
109
110
public UnversionedLargeObjectProfile() => DefaultProfileOperationMode = ProfileOperationMode.MessagePack;
111
}
112
113
[AutoLoadProfile(false)]
114
public partial class MigratingLargeObjectProfile : XFEProfile
115
{
116
[ProfileProperty("CurrentData")]
117
private LargeSettingsDocument currentData = new();
118
119
public MigratingLargeObjectProfile() => DefaultProfileOperationMode = ProfileOperationMode.MessagePack;
120
121
protected override int ProfileSchemaVersion => 1;
122
123
protected override void ConfigureMigrations(ProfileMigrationBuilder migrations) => migrations
124
.RenameProperty(0, "Data", "CurrentData");
125
}
@@ -10,8 +10,8 @@
10
10
11
11
<ItemGroup>
12
12
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.8.1" />
13
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.12.0" PrivateAssets="all" />
14
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.12.0" PrivateAssets="all" />
13
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="5.6.0" PrivateAssets="all" />
14
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="5.6.0" PrivateAssets="all" />
15
15
<PackageReference Include="xunit" Version="2.9.3" />
16
16
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
17
17
<PrivateAssets>all</PrivateAssets>
@@ -0,0 +1,61 @@
1
using MessagePack;
2
3
namespace XFEExtension.NetCore.AutoConfig;
4
5
internal static class MessagePackProfileSerializer
6
{
7
private static readonly MessagePackSerializerOptions DocumentOptions = MessagePackSerializerOptions.Standard
8
.WithSecurity(MessagePackSecurity.UntrustedData);
9
10
public static byte[] Serialize(
11
XFEProfile profile,
12
IReadOnlyDictionary<string, Type> propertyTypes,
13
IReadOnlyDictionary<string, GetValueDelegate> propertyGetters,
14
int version)
15
{
16
var properties = new Dictionary<string, byte[]>(propertyGetters.Count, StringComparer.Ordinal);
17
foreach (var property in propertyGetters)
18
{
19
if (!propertyTypes.TryGetValue(property.Key, out var propertyType))
20
throw new InvalidOperationException($"未找到配置属性“{property.Key}”的类型信息");
21
properties.Add(property.Key, MessagePackSerializer.Serialize(propertyType, property.Value(), profile.MessagePackOptions));
22
}
23
24
return MessagePackSerializer.Serialize(
25
new MessagePackProfileDocument { Version = version, Properties = properties },
26
DocumentOptions);
27
}
28
29
public static MessagePackProfileDocument Deserialize(ReadOnlyMemory<byte> content)
30
{
31
var document = MessagePackSerializer.Deserialize<MessagePackProfileDocument>(content, DocumentOptions)
32
?? throw new MessagePackSerializationException("MessagePack 配置文档为空");
33
document.Properties ??= new Dictionary<string, byte[]>(StringComparer.Ordinal);
34
return document;
35
}
36
37
public static void Populate(
38
XFEProfile profile,
39
IReadOnlyDictionary<string, byte[]> properties,
40
IReadOnlyDictionary<string, Type> propertyTypes,
41
IReadOnlyDictionary<string, SetValueDelegate> propertySetters)
42
{
43
foreach (var property in properties)
44
{
45
if (!propertyTypes.TryGetValue(property.Key, out var propertyType)
46
|| !propertySetters.TryGetValue(property.Key, out var setter))
47
continue;
48
setter(MessagePackSerializer.Deserialize(propertyType, property.Value, profile.MessagePackOptions));
49
}
50
}
51
}
52
53
[MessagePackObject(AllowPrivate = true)]
54
internal sealed class MessagePackProfileDocument
55
{
56
[Key(0)]
57
public int Version { get; set; }
58
59
[Key(1)]
60
public Dictionary<string, byte[]> Properties { get; set; } = new(StringComparer.Ordinal);
61
}
@@ -1,3 +1,4 @@
1
using MessagePack;
1
2
using System.Text.Json;
2
3
using System.Text.Json.Nodes;
3
4
using System.Xml.Linq;
@@ -34,12 +35,81 @@ public sealed class ProfileMigrationContext
34
35
public JsonSerializerOptions JsonOptions { get; }
35
36
}
36
37
38
/// <summary>
39
/// MessagePack 配置迁移步骤的上下文。属性值保持 MessagePack 二进制表示,只有显式读取或写入时才会反序列化。
40
/// </summary>
41
public sealed class MessagePackProfileMigrationContext
42
{
43
private readonly Dictionary<string, byte[]> properties;
44
private readonly MessagePackSerializerOptions options;
45
46
internal MessagePackProfileMigrationContext(int fromVersion, Dictionary<string, byte[]> properties, MessagePackSerializerOptions options)
47
{
48
FromVersion = fromVersion;
49
this.properties = properties;
50
this.options = options;
51
}
52
53
/// <summary>迁移前版本。</summary>
54
public int FromVersion { get; }
55
56
/// <summary>迁移后的版本。</summary>
57
public int ToVersion => FromVersion + 1;
58
59
/// <summary>当前包含的配置属性名称。</summary>
60
public IReadOnlyCollection<string> PropertyNames => properties.Keys;
61
62
/// <summary>判断二进制配置中是否包含指定属性。</summary>
63
public bool ContainsProperty(string propertyName)
64
{
65
ArgumentException.ThrowIfNullOrWhiteSpace(propertyName);
66
return properties.ContainsKey(propertyName);
67
}
68
69
/// <summary>按指定类型读取一个属性。</summary>
70
public T? GetProperty<T>(string propertyName)
71
{
72
ArgumentException.ThrowIfNullOrWhiteSpace(propertyName);
73
if (!properties.TryGetValue(propertyName, out var content))
74
throw new ProfileMigrationException($"MessagePack 配置中不存在属性“{propertyName}”");
75
return MessagePackSerializer.Deserialize<T>(content, options);
76
}
77
78
/// <summary>写入或替换一个属性。</summary>
79
public void SetProperty<T>(string propertyName, T value)
80
{
81
ArgumentException.ThrowIfNullOrWhiteSpace(propertyName);
82
properties[propertyName] = MessagePackSerializer.Serialize(value, options);
83
}
84
85
/// <summary>删除一个属性。</summary>
86
public bool RemoveProperty(string propertyName)
87
{
88
ArgumentException.ThrowIfNullOrWhiteSpace(propertyName);
89
return properties.Remove(propertyName);
90
}
91
92
/// <summary>重命名一个属性;源属性不存在时不执行操作。</summary>
93
public void RenameProperty(string oldName, string newName)
94
{
95
ArgumentException.ThrowIfNullOrWhiteSpace(oldName);
96
ArgumentException.ThrowIfNullOrWhiteSpace(newName);
97
if (!properties.TryGetValue(oldName, out var content))
98
return;
99
if (properties.ContainsKey(newName))
100
throw new ProfileMigrationException($"无法把 MessagePack 字段“{oldName}”重命名为“{newName}”:目标字段已存在");
101
properties.Remove(oldName);
102
properties.Add(newName, content);
103
}
104
}
105
37
106
/// <summary>
38
107
/// 配置迁移注册器。每个步骤负责把版本 N 升级到 N + 1。
39
108
/// </summary>
40
109
public sealed class ProfileMigrationBuilder
41
110
{
42
111
private readonly Dictionary<int, List<Func<ProfileMigrationContext, string>>> migrations = [];
112
private readonly Dictionary<int, List<Action<MessagePackProfileMigrationContext>>> messagePackMigrations = [];
43
113
44
114
/// <summary>
45
115
/// 注册一个结构转换步骤。
@@ -58,19 +128,44 @@ public sealed class ProfileMigrationBuilder
58
128
return this;
59
129
}
60
130
131
/// <summary>
132
/// 注册一个 MessagePack 结构转换步骤。未修改的属性不会被反序列化,适合迁移大型对象配置。
133
/// </summary>
134
public ProfileMigrationBuilder TransformMessagePack(int fromVersion, Action<MessagePackProfileMigrationContext> transform)
135
{
136
ArgumentNullException.ThrowIfNull(transform);
137
if (fromVersion < 0)
138
throw new ArgumentOutOfRangeException(nameof(fromVersion));
139
if (!messagePackMigrations.TryGetValue(fromVersion, out var transforms))
140
{
141
transforms = [];
142
messagePackMigrations.Add(fromVersion, transforms);
143
}
144
transforms.Add(transform);
145
return this;
146
}
147
61
148
/// <summary>
62
149
/// 注册一个无需修改内容的版本升级步骤。
63
150
/// </summary>
64
public ProfileMigrationBuilder NoOp(int fromVersion) => Transform(fromVersion, static context => context.Content);
151
public ProfileMigrationBuilder NoOp(int fromVersion)
152
{
153
Transform(fromVersion, static context => context.Content);
154
TransformMessagePack(fromVersion, static _ => { });
155
return this;
156
}
65
157
66
158
/// <summary>
67
/// 注册跨内置格式的字段重命名步骤。JSON/XML 会同时识别生成的 InstanceXxx 成员名。
159
/// 注册跨内置格式的字段重命名步骤。JSON/XML 会同时识别生成的 InstanceXxx 成员名,
160
/// MessagePack 会直接重命名属性对应的二进制块。
68
161
/// </summary>
69
162
public ProfileMigrationBuilder RenameProperty(int fromVersion, string oldName, string newName)
70
163
{
71
164
ArgumentException.ThrowIfNullOrWhiteSpace(oldName);
72
165
ArgumentException.ThrowIfNullOrWhiteSpace(newName);
73
return Transform(fromVersion, context => RenameProperty(context, oldName, newName));
166
Transform(fromVersion, context => RenameProperty(context, oldName, newName));
167
TransformMessagePack(fromVersion, context => context.RenameProperty(oldName, newName));
168
return this;
74
169
}
75
170
76
171
internal ProfileMigrationResult Apply(int storedVersion, int targetVersion, ProfileOperationMode operationMode, string content, JsonSerializerOptions jsonOptions)
@@ -94,6 +189,28 @@ public sealed class ProfileMigrationBuilder
94
189
return new ProfileMigrationResult(currentContent, storedVersion != targetVersion);
95
190
}
96
191
192
internal MessagePackProfileMigrationResult ApplyMessagePack(
193
int storedVersion,
194
int targetVersion,
195
Dictionary<string, byte[]> properties,
196
MessagePackSerializerOptions options)
197
{
198
if (storedVersion < 0)
199
throw new ProfileMigrationException($"配置版本不能小于零:{storedVersion}");
200
if (storedVersion > targetVersion)
201
throw new ProfileMigrationException($"配置文件版本 {storedVersion} 高于当前支持版本 {targetVersion},不支持自动降级");
202
203
var currentProperties = new Dictionary<string, byte[]>(properties, StringComparer.Ordinal);
204
for (var version = storedVersion; version < targetVersion; version++)
205
{
206
if (!messagePackMigrations.TryGetValue(version, out var transforms) || transforms.Count == 0)
207
throw new ProfileMigrationException($"缺少从版本 {version} 到版本 {version + 1} 的 MessagePack 迁移步骤");
208
foreach (var transform in transforms)
209
transform(new MessagePackProfileMigrationContext(version, currentProperties, options));
210
}
211
return new MessagePackProfileMigrationResult(currentProperties, storedVersion != targetVersion);
212
}
213
97
214
private static string RenameProperty(ProfileMigrationContext context, string oldName, string newName) => context.OperationMode switch
98
215
{
99
216
ProfileOperationMode.XFEDictionary => RenameXfeDictionaryProperty(context.Content, oldName, newName),
@@ -179,6 +296,8 @@ public sealed class ProfileMigrationException : Exception
179
296
180
297
internal readonly record struct ProfileMigrationResult(string Content, bool WasMigrated);
181
298
299
internal readonly record struct MessagePackProfileMigrationResult(Dictionary<string, byte[]> Properties, bool WasMigrated);
300
182
301
internal static class ProfileVersionMetadata
183
302
{
184
303
private const string VersionPropertyName = "$xfeProfileVersion";
@@ -20,5 +20,9 @@ public enum ProfileOperationMode
20
20
/// <summary>
21
21
/// 使用自定义方法加载和存储配置文件
22
22
/// </summary>
23
Custom
23
Custom,
24
/// <summary>
25
/// 使用 MessagePack 二进制格式加载和存储配置文件,适合大型对象
26
/// </summary>
27
MessagePack
24
28
}
@@ -86,6 +86,13 @@ public sealed class ProfileStore<TProfile> where TProfile : XFEProfile, new()
86
86
return current.InstanceExportProfile();
87
87
}
88
88
89
/// <summary>以原始字节导出包含版本元数据的配置;MessagePack 模式不会产生 Base64 中间文本。</summary>
90
public byte[] ExportBytes()
91
{
92
lock (syncRoot)
93
return current.InstanceExportProfileBytes();
94
}
95
89
96
/// <summary>导入配置文本;迁移或验证失败时保持当前实例不变。</summary>
90
97
public void Import(string profileContent)
91
98
{
@@ -94,6 +101,13 @@ public sealed class ProfileStore<TProfile> where TProfile : XFEProfile, new()
94
101
current = (TProfile)current.InstanceImportProfile(profileContent, CreateProfile);
95
102
}
96
103
104
/// <summary>从原始字节导入配置;迁移或验证失败时保持当前实例不变。</summary>
105
public void ImportBytes(ReadOnlyMemory<byte> profileContent)
106
{
107
lock (syncRoot)
108
current = (TProfile)current.InstanceImportProfileBytes(profileContent, CreateProfile);
109
}
110
97
111
/// <summary>
98
112
/// 在实例锁内修改配置,并在修改成功后请求一次合并自动保存。
99
113
/// </summary>