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

XFEExtension.NetCore.AutoConfig

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

公开
关注 0 Fork 0 Star 0
返回提交历史

XFEstudio/XFEExtension.NetCore.AutoConfig

支持 MessagePack 二进制序列化配置文件

新增对 MessagePack 格式的全面支持,适用于大型对象配置,扩展名为 .mpk,`ProfileOperationMode` 增加 MessagePack 选项。`XFEProfile` 增加 MessagePackOptions 属性,默认启用 contractless resolver。配置文件的保存、加载、导入、导出等流程支持 MessagePack,底层采用原始字节读写,避免 Base64。新增导入/导出二进制静态方法,迁移系统支持 MessagePack,属性值保持二进制,迁移方法自动兼容。新增 MessagePackProfileSerializer 辅助类及大对象配置类型和相关单元测试。文档补充 MessagePack 使用说明和 API 变更,代码生成器和测试用例同步更新,升级依赖并引入 MessagePack。

99ef9f2
XFE工作室室长 <mail@xfegzs.com>
提交于

代码差异

14 个文件 +519 -34
Modified AutoConfig.Analyzer.Test/AutoConfig.Analyzer.Test.csproj +1 -1
@@ -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>
Modified README.md +24 -3
@@ -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
Modified README.zh-CN.md +24 -3
@@ -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
Modified XFEExtension.NetCore.AutoConfig.Analyzer/Generator/ProfilePropertyAutoGenerator.cs +12 -1
@@ -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)
Modified XFEExtension.NetCore.AutoConfig.Tests/GeneratorAndCodeFixTests.cs +3 -1
@@ -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]
Added XFEExtension.NetCore.AutoConfig.Tests/MessagePackPersistenceTests.cs +106 -0
@@ -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 }
Modified XFEExtension.NetCore.AutoConfig.Tests/PersistenceProfiles.cs +54 -0
@@ -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 }
Modified XFEExtension.NetCore.AutoConfig.Tests/XFEExtension.NetCore.AutoConfig.Tests.csproj +2 -2
@@ -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>
Added XFEExtension.NetCore.AutoConfig/MessagePackProfileSerializer.cs +61 -0
@@ -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 }
Modified XFEExtension.NetCore.AutoConfig/ProfileMigration.cs +122 -3
@@ -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";
Modified XFEExtension.NetCore.AutoConfig/ProfileOperationMode.cs +5 -1
@@ -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 }
Modified XFEExtension.NetCore.AutoConfig/ProfileStore.cs +14 -0
@@ -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>
Modified XFEExtension.NetCore.AutoConfig/XFEExtension.NetCore.AutoConfig.csproj +2 -1
Modified XFEExtension.NetCore.AutoConfig/XFEProfile.cs +89 -18