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

XFEExtension.NetCore.AutoConfig

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

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

XFEstudio/XFEExtension.NetCore.AutoConfig

调整 ProfileProperty 为静态 partial 属性,完善序列化支持

将 .NET 10/C# 14 的 ProfileProperty 部分属性由实例 partial 属性调整为静态 partial 属性,保留静态访问方式。生成器为每个静态 partial 属性自动生成实例桥接属性 InstanceXxx,仅供序列化器使用,保证初始值同步。修正 XML/JSON 持久化时元素/属性名为声明名。分析器和代码修复工具同步升级,自动迁移相关引用。文档和测试同步更新,版本号提升至 4.1.2。

782b820
XFE工作室室长 <mail@xfegzs.com>
提交于

代码差异

9 个文件 +120 -118
Modified README.md +11 -12
@@ -53,26 +53,26 @@ class Program
53 53
54 54 ### .NET 10 Partial Properties
55 55
56 Projects targeting .NET 10 or later with C# 14 can declare partial instance properties for the generator to implement. The generated implementation uses `field` for backing storage, synchronization, collection binding, and automatic saving:
56 Projects targeting .NET 10 or later with C# 14 can declare static partial properties for the generator to implement. The generated implementation preserves initializers and provides instance persistence storage, synchronization, collection binding, and automatic saving:
57 57
58 58 ```csharp
59 59 [AutoLoadProfile]
60 60 partial class SystemProfile : XFEProfile
61 61 {
62 62 [ProfileProperty]
63 public partial string Name { get; set; } = string.Empty;
63 public static partial string Name { get; set; } = string.Empty;
64 64
65 65 [ProfileProperty]
66 public partial int Age { get; set; }
66 public static partial int Age { get; set; }
67 67 }
68 68
69 SystemProfile.Current.Name = "Test";
70 Console.WriteLine(SystemProfile.Current.Age);
69 SystemProfile.Name = "Test";
70 Console.WriteLine(SystemProfile.Age);
71 71 ```
72 72
73 Partial profile properties use the natural name `Xxx`, without an `Instance` prefix, and persist under the same name. Because C# cannot declare both an instance `Name` and a static `Name` on one type, partial-property mode does not generate a same-name static facade; access it through `SystemProfile.Current.Name`. Field mode retains the existing static `Name` and instance `InstanceName` APIs.
73 Partial profile properties use the natural name `Xxx`, without an `Instance` prefix, retain the `SystemProfile.Xxx` access pattern, and persist under the same name. The generator also creates `InstanceXxx` as the instance bridge required by serializers; application code does not need to access it.
74 74
75 The field syntax remains supported. In a .NET 10+ project using C# 14, analyzer diagnostic `XFE0003` suggests eligible fields and offers the “Convert to .NET 10 partial profile property” code fix. The fix preserves the initializer, updates field references, rewrites existing static accesses to `Current.Xxx`, updates get/set hook strings, and retargets other field attributes with `field:`. The equivalent manual migration is:
75 The field syntax remains supported. In a .NET 10+ project using C# 14, analyzer diagnostic `XFE0003` suggests eligible fields and offers the “Convert to .NET 10 partial profile property” code fix. The fix preserves the initializer and existing `SystemProfile.Xxx` calls, updates field references and get/set hook strings, and retargets other field attributes with `field:`. The equivalent manual migration is:
76 76
77 77 ```csharp
78 78 // Before
@@ -81,7 +81,7 @@ string name = "Guest";
81 81
82 82 // .NET 10 / C# 14
83 83 [ProfileProperty]
84 public partial string Name { get; set; } = "Guest";
84 public static partial string Name { get; set; } = "Guest";
85 85 ```
86 86
87 87 ---
@@ -131,7 +131,7 @@ partial class SystemProfile : XFEProfile
131 131
132 132 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.
133 133
134 XML element names use the profile property name (for example, `<Value>`). Field mode generates the C# instance property `InstanceValue`, while partial-property mode directly uses the declared `Value` property.
134 XML element names always use the profile property name (for example, `<Value>`) and never include the `Instance` prefix from the serializer bridge.
135 135
136 136 ```csharp
137 137 [AutoLoadProfile]
@@ -482,9 +482,8 @@ For every `partial` class that inherits `XFEProfile` and uses `[ProfileProperty]
482 482 | `ExportProfileBytes()` | `static byte[]` | Exports raw config bytes; preferred for MessagePack |
483 483 | `ImportProfile(string)` | `static void` | Imports config from a string |
484 484 | `ImportProfileBytes(ReadOnlyMemory<byte>)` | `static void` | Imports config directly from bytes |
485 | `Xxx` (field mode) | `static T` | Auto-generated static property; saves on set |
486 | `InstanceXxx` (field mode) | `T` (instance) | Instance property corresponding to a field |
487 | `Current.Xxx` (partial-property mode) | `T` (instance) | User-declared natural-name partial property; saves on set |
485 | `Xxx` (field or partial-property mode) | `static T` | Static configuration access point; saves on set |
486 | `InstanceXxx` | `T` (instance) | Generated instance bridge used by serializers |
488 487 | `GetXxxProperty()` | `static partial void` | Invoked when the property is read |
489 488 | `SetXxxProperty(ref T)` | `static partial void` | Invoked when the property is written |
490 489
Modified README.zh-CN.md +11 -12
@@ -53,26 +53,26 @@ class Program
53 53
54 54 ### .NET 10 部分属性
55 55
56 目标项目为 .NET 10 或更高版本并使用 C# 14 时,可以直接声明由生成器实现的部分实例属性。生成器会用 `field` 实现后备存储、并发锁、集合绑定与自动保存:
56 目标项目为 .NET 10 或更高版本并使用 C# 14 时,可以直接声明由生成器实现的静态部分属性。生成器会保留属性初始值,并实现实例持久化存储、并发锁、集合绑定与自动保存:
57 57
58 58 ```csharp
59 59 [AutoLoadProfile]
60 60 partial class SystemProfile : XFEProfile
61 61 {
62 62 [ProfileProperty]
63 public partial string Name { get; set; } = string.Empty;
63 public static partial string Name { get; set; } = string.Empty;
64 64
65 65 [ProfileProperty]
66 public partial int Age { get; set; }
66 public static partial int Age { get; set; }
67 67 }
68 68
69 SystemProfile.Current.Name = "Test";
70 Console.WriteLine(SystemProfile.Current.Age);
69 SystemProfile.Name = "Test";
70 Console.WriteLine(SystemProfile.Age);
71 71 ```
72 72
73 部分属性直接使用自然名称 `Xxx`,不需要 `Instance` 前缀,持久化名称同样为 `Xxx`。由于 C# 不允许同一个类型同时声明实例 `Name` 和静态 `Name`,部分属性模式不再生成同名静态门面,应通过 `SystemProfile.Current.Name` 访问。字段模式仍保留原有的静态 `Name` 和实例 `InstanceName` API。
73 部分属性直接使用自然名称 `Xxx`,不需要 `Instance` 前缀,调用方式仍为 `SystemProfile.Xxx`,持久化名称同样为 `Xxx`。生成器会另外创建 `InstanceXxx` 作为序列化所需的实例桥接成员,但业务代码无需通过它访问配置。
74 74
75 旧的字段写法继续受到支持。对于目标为 .NET 10+ 且启用 C# 14 的项目,分析器会以 `XFE0003` 提示可升级字段,并提供“转换为 .NET 10 部分配置属性”代码修复。修复会保留初始值、更新字段引用,把已有的静态访问改为 `Current.Xxx`,并更新 get/set 特性中的字段名;原字段上的其他特性会改为 `field:` 目标。手动升级时可按以下方式转换:
75 旧的字段写法继续受到支持。对于目标为 .NET 10+ 且启用 C# 14 的项目,分析器会以 `XFE0003` 提示可升级字段,并提供“转换为 .NET 10 部分配置属性”代码修复。修复会保留初始值和 `SystemProfile.Xxx` 静态调用,更新字段引用及 get/set 特性中的字段名;原字段上的其他特性会改为 `field:` 目标。手动升级时可按以下方式转换:
76 76
77 77 ```csharp
78 78 // 旧写法
@@ -81,7 +81,7 @@ string name = "Guest";
81 81
82 82 // .NET 10 / C# 14 写法
83 83 [ProfileProperty]
84 public partial string Name { get; set; } = "Guest";
84 public static partial string Name { get; set; } = "Guest";
85 85 ```
86 86
87 87 ---
@@ -131,7 +131,7 @@ partial class SystemProfile : XFEProfile
131 131
132 132 重写 `ProfileSchemaVersion`,并在 `ConfigureMigrations` 中注册每个 `N -> N + 1` 步骤。没有版本元数据的旧文件视为版本 `0`。XFE 字典、JSON、XML、MessagePack 四种内置格式会自动保存版本;`RenameProperty` 同时处理所有内置格式。MessagePack 的自定义结构迁移使用 `TransformMessagePack`,未修改的属性会保持二进制形式,不会被反序列化。
133 133
134 XML 节点会使用配置属性名称(例如 `<Value>`)。字段模式生成的 C# 实例属性为 `InstanceValue`,部分属性模式则直接使用用户声明的 `Value`。
134 XML 节点始终使用配置属性名称(例如 `<Value>`),不会包含实例桥接成员的 `Instance` 前缀。
135 135
136 136 ```csharp
137 137 [AutoLoadProfile]
@@ -475,9 +475,8 @@ SystemProfile.ImportProfile(exported); // 从字符串导入配置
475 475 | `ExportProfileBytes()` | `static byte[]` | 直接导出配置字节,适合 MessagePack |
476 476 | `ImportProfile(string)` | `static void` | 从字符串导入配置 |
477 477 | `ImportProfileBytes(ReadOnlyMemory<byte>)` | `static void` | 直接从配置字节导入 |
478 | `Xxx`(字段模式)| `static T` | 自动生成的静态属性,读写时自动持久化 |
479 | `InstanceXxx`(字段模式)| `T`(实例)| 字段对应的实例属性 |
480 | `Current.Xxx`(部分属性模式)| `T`(实例)| 用户声明的自然名称部分属性,读写时自动持久化 |
478 | `Xxx`(字段或部分属性模式)| `static T` | 配置的静态访问入口,读写时自动持久化 |
479 | `InstanceXxx` | `T`(实例)| 生成器提供给序列化器的实例桥接属性 |
481 480 | `GetXxxProperty()` | `static partial void` | get 钩子分部方法 |
482 481 | `SetXxxProperty(ref T)` | `static partial void` | set 钩子分部方法 |
483 482
Modified XFEExtension.NetCore.AutoConfig.Analyzer/Diagnostics/AutoConfigDiagnostics.cs +2 -2
@@ -46,11 +46,11 @@ public sealed class AutoConfigDiagnostics : DiagnosticAnalyzer
46 46 public static readonly DiagnosticDescriptor FieldCanUsePartialProperty = new(
47 47 FieldCanUsePartialPropertyId,
48 48 "配置字段可升级为部分属性",
49 "配置字段“{0}”可升级为 .NET 10 部分属性“{1}”,以减少显式字段和生成成员",
49 "配置字段“{0}”可升级为 .NET 10 静态部分属性“{1}”,并保留现有静态调用方式",
50 50 "XFEExtension.NetCore.AutoConfig.Diagnostics",
51 51 DiagnosticSeverity.Info,
52 52 true,
53 "目标项目为 .NET 10 或更高版本时,可使用 C# 14 field-backed partial property,并通过代码修复完成迁移.");
53 "目标项目为 .NET 10 或更高版本时,可使用 C# 14 static field-backed partial property,并通过代码修复完成迁移.");
54 54
55 55 public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [AddGetNoResultError, AddSetNoSetResultWarning, FieldCanUsePartialProperty];
56 56
Modified XFEExtension.NetCore.AutoConfig.Analyzer/Generator/ProfilePropertyAutoGenerator.cs +57 -25
@@ -8,6 +8,7 @@ using System.Collections.Immutable;
8 8 using System.Linq;
9 9 using System.Security.Cryptography;
10 10 using System.Text;
11 using System.Text.RegularExpressions;
11 12
12 13 namespace XFEExtension.NetCore.AutoConfig.Analyzer.Generator;
13 14
@@ -58,7 +59,7 @@ public sealed class ProfilePropertyAutoGenerator : IIncrementalGenerator
58 59 DiagnosticSeverity.Error, true);
59 60
60 61 private static readonly DiagnosticDescriptor UnsupportedPartialProperty = new(
61 "XFE1009", "不支持的部分配置属性", "属性“{0}”必须使用 C# 14 的 public partial Xxx {{ get; set; }} 声明,并且不能是 static、required、virtual 或显式接口实现", "XFEExtension.NetCore.AutoConfig.Generator",
62 "XFE1009", "不支持的部分配置属性", "属性“{0}”必须使用 C# 14 的 public static partial Xxx {{ get; set; }} 声明,并且不能是 required、virtual 或显式接口实现", "XFEExtension.NetCore.AutoConfig.Generator",
62 63 DiagnosticSeverity.Error, true);
63 64
64 65 public void Initialize(IncrementalGeneratorInitializationContext context)
@@ -199,7 +200,7 @@ public sealed class ProfilePropertyAutoGenerator : IIncrementalGenerator
199 200 context.ReportDiagnostic(Diagnostic.Create(GeneratedPropertyConflict, GetLocation(field), field.Name, propertyName));
200 201 continue;
201 202 }
202 models.Add(new ProfileMemberModel(field, field.Type, propertyName, field.Name, false, GetAttributeStrings(field, ProfilePropertyAddGetAttributeName), GetAttributeStrings(field, ProfilePropertyAddSetAttributeName)));
203 models.Add(new ProfileMemberModel(field, field.Type, propertyName, field.Name, false, null, GetAttributeStrings(field, ProfilePropertyAddGetAttributeName), GetAttributeStrings(field, ProfilePropertyAddSetAttributeName)));
203 204 }
204 205
205 206 var invalidModels = new HashSet<ProfileMemberModel>();
@@ -219,7 +220,7 @@ public sealed class ProfilePropertyAutoGenerator : IIncrementalGenerator
219 220 }
220 221
221 222 var names = model.IsPartialProperty
222 ? new[] { "Get" + model.PropertyName + "Property", "Set" + model.PropertyName + "Property" }
223 ? new[] { model.StorageMemberName, "Instance" + model.PropertyName, "Get" + model.PropertyName + "Property", "Set" + model.PropertyName + "Property" }
223 224 : new[] { model.PropertyName, "Instance" + model.PropertyName, "Get" + model.PropertyName + "Property", "Set" + model.PropertyName + "Property" };
224 225 foreach (var name in names)
225 226 {
@@ -269,9 +270,9 @@ public sealed class ProfilePropertyAutoGenerator : IIncrementalGenerator
269 270 && parseOptions is not null
270 271 && parseOptions.LanguageVersion >= LanguageVersion.CSharp14
271 272 && declaration.Modifiers.Any(SyntaxKind.PartialKeyword)
272 && declaration.Modifiers.All(static modifier => modifier.IsKind(SyntaxKind.PublicKeyword) || modifier.IsKind(SyntaxKind.PartialKeyword))
273 && declaration.Modifiers.All(static modifier => modifier.IsKind(SyntaxKind.PublicKeyword) || modifier.IsKind(SyntaxKind.StaticKeyword) || modifier.IsKind(SyntaxKind.PartialKeyword))
273 274 && property.DeclaredAccessibility == Accessibility.Public
274 && !property.IsStatic
275 && property.IsStatic
275 276 && !property.IsAbstract
276 277 && !property.IsVirtual
277 278 && !property.IsOverride
@@ -295,8 +296,9 @@ public sealed class ProfilePropertyAutoGenerator : IIncrementalGenerator
295 296 property,
296 297 property.Type,
297 298 propertyName,
298 property.Name,
299 "__profileStorage_" + property.Name,
299 300 true,
301 declaration?.Initializer?.Value.ToFullString().Trim(),
300 302 GetAttributeStrings(property, ProfilePropertyAddGetAttributeName),
301 303 GetAttributeStrings(property, ProfilePropertyAddSetAttributeName));
302 304 }
@@ -323,6 +325,13 @@ public sealed class ProfilePropertyAutoGenerator : IIncrementalGenerator
323 325 : SymbolDisplay.FormatLiteral(configuredPath, true)).AppendLine(";");
324 326 builder.AppendLine(" private static string __profileExtension = string.Empty;");
325 327 builder.Append(" private static ").Append(typeName).AppendLine(" __current = null!;");
328 foreach (var member in members.Where(static member => member.IsPartialProperty))
329 {
330 var typeDisplay = member.Type.ToDisplayString(DeclarationTypeDisplayFormat);
331 builder.Append(" private ").Append(typeDisplay).Append(' ').Append(EscapeIdentifier(member.StorageMemberName))
332 .Append(" = __current is null ? ").Append(EscapeIdentifier(member.Member.Name)).Append(" : ")
333 .Append(member.InitializerExpression ?? "default!").AppendLine(";");
334 }
326 335 builder.AppendLine();
327 336 builder.Append(" static ").Append(typeName).AppendLine("()");
328 337 builder.AppendLine(" {");
@@ -492,7 +501,7 @@ public sealed class ProfilePropertyAutoGenerator : IIncrementalGenerator
492 501
493 502 if (member.IsPartialProperty)
494 503 {
495 AppendPartialProfileProperty(builder, member, storageMemberName, getMethodName, setMethodName, typeDisplay, xmlElementNameLiteral);
504 AppendPartialProfileProperty(builder, member, storageMemberName, instancePropertyName, getMethodName, setMethodName, typeDisplay, xmlElementNameLiteral);
496 505 return;
497 506 }
498 507
@@ -548,49 +557,70 @@ public sealed class ProfilePropertyAutoGenerator : IIncrementalGenerator
548 557 private static void AppendPartialProfileProperty(
549 558 StringBuilder builder,
550 559 ProfileMemberModel member,
551 string propertyName,
560 string storageMemberName,
561 string instancePropertyName,
552 562 string getMethodName,
553 563 string setMethodName,
554 564 string typeDisplay,
555 565 string xmlElementNameLiteral)
556 566 {
567 var declaredPropertyName = EscapeIdentifier(member.Member.Name);
557 568 builder.AppendLine(" [global::XFEExtension.NetCore.AutoConfig.ProfileFieldAutoGenerateAttribute]");
558 builder.Append(" [global::System.Xml.Serialization.XmlElementAttribute(").Append(xmlElementNameLiteral).AppendLine(")]");
559 if (member.PropertyName != member.StorageMemberName
560 && GetAttribute(member.Member, "System.Text.Json.Serialization.JsonPropertyNameAttribute") is null)
561 {
562 builder.Append(" [global::System.Text.Json.Serialization.JsonPropertyNameAttribute(").Append(xmlElementNameLiteral).AppendLine(")]");
563 }
564 builder.Append(" public partial ").Append(typeDisplay).Append(' ').Append(propertyName).AppendLine();
569 builder.Append(" public static partial ").Append(typeDisplay).Append(' ').Append(declaredPropertyName).AppendLine();
565 570 builder.AppendLine(" {");
566 571 builder.AppendLine(" get");
567 572 builder.AppendLine(" {");
568 builder.AppendLine(" lock (ProfileSyncRoot)");
573 builder.AppendLine(" if (__current is null) return field;");
574 builder.AppendLine(" lock (__profileInstanceSyncRoot)");
575 builder.AppendLine(" lock (__current.ProfileSyncRoot)");
569 576 builder.AppendLine(" {");
570 577 builder.Append(" ").Append(getMethodName).AppendLine("();");
571 578 if (member.GetStatements.Length == 0)
572 builder.AppendLine(" return field;");
579 builder.Append(" return __current.").Append(storageMemberName).AppendLine(";");
573 580 else
574 AppendStatements(builder, member.GetStatements, 16);
581 AppendPartialStatements(builder, member.GetStatements, storageMemberName, 16);
575 582 builder.AppendLine(" }");
576 583 builder.AppendLine(" }");
577 584 builder.AppendLine(" set");
578 585 builder.AppendLine(" {");
579 builder.AppendLine(" lock (ProfileSyncRoot)");
586 builder.AppendLine(" if (__current is null) { field = value; return; }");
587 builder.AppendLine(" lock (__profileInstanceSyncRoot)");
588 builder.AppendLine(" lock (__current.ProfileSyncRoot)");
580 589 builder.AppendLine(" {");
581 590 builder.Append(" ").Append(setMethodName).AppendLine("(ref value);");
582 591 if (member.SetStatements.Length == 0)
583 builder.AppendLine(" field = value;");
592 builder.Append(" __current.").Append(storageMemberName).AppendLine(" = value;");
584 593 else
585 AppendStatements(builder, member.SetStatements, 16);
594 AppendPartialStatements(builder, member.SetStatements, storageMemberName, 16);
586 595 if (IsProfileOwnedCollection(member.Type))
587 builder.AppendLine(" this.__BindProfileOwnedCollections();");
588 builder.AppendLine(" if (global::System.Object.ReferenceEquals(this, __current))");
589 builder.AppendLine(" this.InstanceRequestSaveProfile();");
596 builder.AppendLine(" __current.__BindProfileOwnedCollections();");
597 builder.AppendLine(" __current.InstanceRequestSaveProfile();");
590 598 builder.AppendLine(" }");
591 599 builder.AppendLine(" }");
592 600 builder.AppendLine(" }");
593 601 builder.AppendLine();
602 builder.AppendLine(" [global::XFEExtension.NetCore.AutoConfig.ProfileFieldAutoGenerateAttribute]");
603 builder.Append(" [global::System.Xml.Serialization.XmlElementAttribute(").Append(xmlElementNameLiteral).AppendLine(")]");
604 builder.Append(" [global::System.Text.Json.Serialization.JsonPropertyNameAttribute(").Append(xmlElementNameLiteral).AppendLine(")]");
605 builder.Append(" public ").Append(typeDisplay).Append(' ').Append(instancePropertyName).AppendLine();
606 builder.AppendLine(" {");
607 builder.Append(" get { lock (ProfileSyncRoot) return this.").Append(storageMemberName).AppendLine("; }");
608 if (IsProfileOwnedCollection(member.Type))
609 {
610 builder.Append(" set { lock (ProfileSyncRoot) { this.").Append(storageMemberName).AppendLine(" = value; this.__BindProfileOwnedCollections(); } }");
611 }
612 else
613 {
614 builder.Append(" set { lock (ProfileSyncRoot) this.").Append(storageMemberName).AppendLine(" = value; }");
615 }
616 builder.AppendLine(" }");
617 builder.AppendLine();
618 }
619
620 private static void AppendPartialStatements(StringBuilder builder, ImmutableArray<string> statements, string storageMemberName, int indentation)
621 {
622 var storageExpression = "__current." + storageMemberName;
623 AppendStatements(builder, [.. statements.Select(statement => Regex.Replace(statement, @"\bfield\b", storageExpression))], indentation);
594 624 }
595 625
596 626 private static void AppendStatements(StringBuilder builder, ImmutableArray<string> statements, int indentation)
@@ -697,13 +727,14 @@ public sealed class ProfilePropertyAutoGenerator : IIncrementalGenerator
697 727
698 728 private sealed class ProfileMemberModel
699 729 {
700 public ProfileMemberModel(ISymbol member, ITypeSymbol type, string propertyName, string storageMemberName, bool isPartialProperty, ImmutableArray<string> getStatements, ImmutableArray<string> setStatements)
730 public ProfileMemberModel(ISymbol member, ITypeSymbol type, string propertyName, string storageMemberName, bool isPartialProperty, string? initializerExpression, ImmutableArray<string> getStatements, ImmutableArray<string> setStatements)
701 731 {
702 732 Member = member;
703 733 Type = type;
704 734 PropertyName = propertyName;
705 735 StorageMemberName = storageMemberName;
706 736 IsPartialProperty = isPartialProperty;
737 InitializerExpression = initializerExpression;
707 738 GetStatements = getStatements;
708 739 SetStatements = setStatements;
709 740 }
@@ -713,6 +744,7 @@ public sealed class ProfilePropertyAutoGenerator : IIncrementalGenerator
713 744 public string PropertyName { get; }
714 745 public string StorageMemberName { get; }
715 746 public bool IsPartialProperty { get; }
747 public string? InitializerExpression { get; }
716 748 public ImmutableArray<string> GetStatements { get; }
717 749 public ImmutableArray<string> SetStatements { get; }
718 750 }
Modified XFEExtension.NetCore.AutoConfig.CodeFix/AutoConfigCodeFixProvider.cs +1 -51
@@ -22,7 +22,6 @@ namespace XFEExtension.NetCore.AutoConfig.CodeFix;
22 22 public sealed class AutoConfigCodeFixProvider : CodeFixProvider
23 23 {
24 24 private const string ProfilePropertyAttributeName = "XFEExtension.NetCore.AutoConfig.ProfilePropertyAttribute";
25 private const string GeneratedProfilePropertyAttributeName = "XFEExtension.NetCore.AutoConfig.ProfileFieldAutoGenerateAttribute";
26 25 private const string AddGetAttributeName = "XFEExtension.NetCore.AutoConfig.ProfilePropertyAddGetAttribute";
27 26 private const string AddSetAttributeName = "XFEExtension.NetCore.AutoConfig.ProfilePropertyAddSetAttribute";
28 27
@@ -100,10 +99,6 @@ public sealed class AutoConfigCodeFixProvider : CodeFixProvider
100 99 return document.Project.Solution;
101 100
102 101 var originalFieldName = field.Name;
103 var generatedStaticFacade = field.ContainingType.GetMembers(propertyName)
104 .OfType<IPropertySymbol>()
105 .FirstOrDefault(static property => property.IsStatic
106 && property.GetAttributes().Any(static attribute => attribute.AttributeClass?.ToDisplayString() == GeneratedProfilePropertyAttributeName));
107 102 var profileAttributeNames = new HashSet<string>(StringComparer.Ordinal);
108 103 var getHookAttributeNames = new HashSet<string>(StringComparer.Ordinal);
109 104 var setHookAttributeNames = new HashSet<string>(StringComparer.Ordinal);
@@ -124,9 +119,6 @@ public sealed class AutoConfigCodeFixProvider : CodeFixProvider
124 119 solution = solution.WithDocumentSyntaxRoot(
125 120 document.Id,
126 121 root.ReplaceNode(fieldDeclaration, fieldDeclaration.WithAdditionalAnnotations(declarationAnnotation)));
127 if (generatedStaticFacade is not null)
128 solution = await RewriteStaticFacadeReferencesAsync(solution, generatedStaticFacade, cancellationToken).ConfigureAwait(false);
129
130 122 var referencedSymbols = await SymbolFinder.FindReferencesAsync(field, solution, cancellationToken).ConfigureAwait(false);
131 123 var referenceLocations = referencedSymbols
132 124 .SelectMany(static referencedSymbol => referencedSymbol.Locations)
@@ -170,7 +162,7 @@ public sealed class AutoConfigCodeFixProvider : CodeFixProvider
170 162 setHookAttributeNames);
171 163 var partialProperty = SyntaxFactory.PropertyDeclaration(currentFieldDeclaration.Declaration.Type.WithoutTrivia(), SyntaxFactory.Identifier(propertyName))
172 164 .WithAttributeLists(attributeLists)
173 .WithModifiers(SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.PublicKeyword), SyntaxFactory.Token(SyntaxKind.PartialKeyword)))
165 .WithModifiers(SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.PublicKeyword), SyntaxFactory.Token(SyntaxKind.StaticKeyword), SyntaxFactory.Token(SyntaxKind.PartialKeyword)))
174 166 .WithAccessorList(SyntaxFactory.AccessorList(SyntaxFactory.List([
175 167 SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration).WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)),
176 168 SyntaxFactory.AccessorDeclaration(SyntaxKind.SetAccessorDeclaration).WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken))
@@ -184,48 +176,6 @@ public sealed class AutoConfigCodeFixProvider : CodeFixProvider
184 176 return solution.WithDocumentSyntaxRoot(declarationDocument.Id, updatedRoot);
185 177 }
186 178
187 private static async Task<Solution> RewriteStaticFacadeReferencesAsync(Solution solution, IPropertySymbol generatedStaticFacade, CancellationToken cancellationToken)
188 {
189 var referencedSymbols = await SymbolFinder.FindReferencesAsync(generatedStaticFacade, solution, cancellationToken).ConfigureAwait(false);
190 var referenceLocations = referencedSymbols
191 .SelectMany(static referencedSymbol => referencedSymbol.Locations)
192 .Where(static location => location.Location.IsInSource)
193 .GroupBy(static location => location.Document.Id);
194 foreach (var documentLocations in referenceLocations)
195 {
196 var referenceDocument = solution.GetDocument(documentLocations.Key);
197 var referenceRoot = referenceDocument is null ? null : await referenceDocument.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
198 if (referenceDocument is null || referenceRoot is null)
199 continue;
200 var referenceNodes = documentLocations
201 .Select(location => referenceRoot.FindNode(location.Location.SourceSpan, getInnermostNodeForTie: true))
202 .Select(node => node.AncestorsAndSelf().OfType<IdentifierNameSyntax>().FirstOrDefault())
203 .Where(static node => node is not null)
204 .Select(static node => node!.Parent is MemberAccessExpressionSyntax memberAccess && memberAccess.Name == node ? (SyntaxNode)memberAccess : node)
205 .Distinct()
206 .ToArray();
207 if (referenceNodes.Length == 0)
208 continue;
209 var rewrittenRoot = referenceRoot.ReplaceNodes(referenceNodes, static (_, rewritten) => rewritten switch
210 {
211 MemberAccessExpressionSyntax memberAccess => memberAccess.WithExpression(
212 SyntaxFactory.MemberAccessExpression(
213 SyntaxKind.SimpleMemberAccessExpression,
214 memberAccess.Expression,
215 SyntaxFactory.IdentifierName("Current"))).WithAdditionalAnnotations(Formatter.Annotation),
216 IdentifierNameSyntax identifier => SyntaxFactory.MemberAccessExpression(
217 SyntaxKind.SimpleMemberAccessExpression,
218 SyntaxFactory.IdentifierName("Current"),
219 identifier.WithoutTrivia())
220 .WithTriviaFrom(identifier)
221 .WithAdditionalAnnotations(Formatter.Annotation),
222 _ => rewritten
223 });
224 solution = solution.WithDocumentSyntaxRoot(referenceDocument.Id, rewrittenRoot);
225 }
226 return solution;
227 }
228
229 179 private static SyntaxList<AttributeListSyntax> RewriteHookAttributeStrings(
230 180 SyntaxList<AttributeListSyntax> attributeLists,
231 181 string fieldName,
Modified XFEExtension.NetCore.AutoConfig.Tests/GeneratorAndCodeFixTests.cs +13 -10
@@ -80,17 +80,17 @@ public sealed class GeneratorAndCodeFixTests
80 80 public partial class Settings : XFEProfile
81 81 {
82 82 [ProfileProperty]
83 public partial string Name { get; set; } = "default";
83 public static partial string Name { get; set; } = "default";
84 84 }
85 85 """;
86 86
87 87 var (result, outputCompilation) = RunGenerator(source);
88 88 var generatedSource = result.Results.Single().GeneratedSources.Single().SourceText.ToString();
89 89
90 Assert.DoesNotContain("public static string Name", generatedSource);
91 Assert.Contains("public partial string Name", generatedSource);
90 Assert.Contains("public static partial string Name", generatedSource);
91 Assert.Contains("public string InstanceName", generatedSource);
92 92 Assert.Contains("return field;", generatedSource);
93 Assert.Contains("this.InstanceRequestSaveProfile();", generatedSource);
93 Assert.Contains("__current.InstanceRequestSaveProfile();", generatedSource);
94 94 Assert.DoesNotContain(outputCompilation.GetDiagnostics(), diagnostic => diagnostic.Severity == DiagnosticSeverity.Error);
95 95 }
96 96
@@ -103,7 +103,7 @@ public sealed class GeneratorAndCodeFixTests
103 103 public partial class Settings : XFEProfile
104 104 {
105 105 [ProfileProperty("StoredName")]
106 public partial string Name { get; set; } = "default";
106 public static partial string Name { get; set; } = "default";
107 107 }
108 108 """;
109 109
@@ -113,7 +113,8 @@ public sealed class GeneratorAndCodeFixTests
113 113 Assert.Contains("XmlElementAttribute(\"StoredName\")", generatedSource);
114 114 Assert.Contains("JsonPropertyNameAttribute(\"StoredName\")", generatedSource);
115 115 Assert.Contains("PropertyInfoDictionary[\"StoredName\"]", generatedSource);
116 Assert.Contains("public partial string Name", generatedSource);
116 Assert.Contains("public static partial string Name", generatedSource);
117 Assert.Contains("public string InstanceStoredName", generatedSource);
117 118 Assert.DoesNotContain(outputCompilation.GetDiagnostics(), diagnostic => diagnostic.Severity == DiagnosticSeverity.Error);
118 119 }
119 120
@@ -124,7 +125,8 @@ public sealed class GeneratorAndCodeFixTests
124 125 [InlineData("public partial class Settings : XFEProfile { public Settings(int value) { } [ProfileProperty] private int value; }", "XFE1005")]
125 126 [InlineData("public partial class Settings : XFEProfile { public static Settings Current { get; set; } = null!; [ProfileProperty] private int value; }", "XFE1006")]
126 127 [InlineData("public partial class Settings { [ProfileProperty] private int value; }", "XFE1008")]
127 [InlineData("public partial class Settings : XFEProfile { [ProfileProperty] public partial int Value { get; init; } }", "XFE1009")]
128 [InlineData("public partial class Settings : XFEProfile { [ProfileProperty] public static partial int Value { get; init; } }", "XFE1009")]
129 [InlineData("public partial class Settings : XFEProfile { [ProfileProperty] public partial int Value { get; set; } }", "XFE1009")]
128 130 public void GeneratorReportsActionableUsageDiagnostics(string declaration, string diagnosticId)
129 131 {
130 132 var source = "using XFEExtension.NetCore.AutoConfig;\n" + declaration;
@@ -270,7 +272,7 @@ public sealed class GeneratorAndCodeFixTests
270 272 var changedDocument = changedSolution.GetDocument(documentId)!;
271 273 var changedText = (await changedDocument.GetTextAsync()).ToString();
272 274
273 Assert.Contains("public partial string Value", changedText);
275 Assert.Contains("public static partial string Value", changedText);
274 276 Assert.Contains("field:", changedText);
275 277 Assert.Contains("System.NonSerialized", changedText);
276 278 Assert.Contains("[Profile]", changedText);
@@ -278,7 +280,8 @@ public sealed class GeneratorAndCodeFixTests
278 280 Assert.Contains("= \"default\";", changedText);
279 281 Assert.Contains("ReadValue() => Value", changedText);
280 282 var consumerText = (await changedSolution.GetDocument(consumerDocumentId)!.GetTextAsync()).ToString();
281 Assert.Contains("Settings.Current.Value", consumerText);
283 Assert.Contains("Settings.Value", consumerText);
284 Assert.DoesNotContain("Settings.Current.Value", consumerText);
282 285 changedSolution = changedSolution.RemoveDocument(generatedDocumentId);
283 286 changedDocument = changedSolution.GetDocument(documentId)!;
284 287 var changedCompilation = (CSharpCompilation)(await changedDocument.Project.GetCompilationAsync())!;
@@ -300,7 +303,7 @@ public sealed class GeneratorAndCodeFixTests
300 303 ? $$"""
301 304 [ProfileProperty]
302 305 [{{attributeName}}("{{existingCode}}")]
303 public partial int Value { get; set; }
306 public static partial int Value { get; set; }
304 307 """
305 308 : $$"""
306 309 [ProfileProperty]
Modified XFEExtension.NetCore.AutoConfig.Tests/PersistenceProfiles.cs +11 -1
@@ -11,11 +11,21 @@ public partial class XfeRoundTripProfile : XFEProfile
11 11 public partial class PartialPropertyProfile : XFEProfile
12 12 {
13 13 [ProfileProperty]
14 public partial string Value { get; set; } = "partial-default";
14 public static partial string Value { get; set; } = "partial-default";
15 15
16 16 public PartialPropertyProfile() => DefaultProfileOperationMode = ProfileOperationMode.Xml;
17 17 }
18 18
19 [AutoLoadProfile(false)]
20 public partial class PartialPropertyDefaultProfile : XFEProfile
21 {
22 [ProfileProperty]
23 public static partial string Value { get; set; } = "partial-default";
24
25 [ProfileProperty]
26 public static partial List<string> Values { get; set; } = [];
27 }
28
19 29 [AutoLoadProfile(false)]
20 30 public partial class JsonRoundTripProfile : XFEProfile
21 31 {
Modified XFEExtension.NetCore.AutoConfig.Tests/PersistenceTests.cs +13 -4
@@ -4,6 +4,15 @@ namespace XFEExtension.NetCore.AutoConfig.Tests;
4 4
5 5 public sealed class PersistenceTests
6 6 {
7 [Fact]
8 public void StaticPartialPropertyPreservesInitializer()
9 {
10 Assert.Equal("partial-default", PartialPropertyDefaultProfile.Value);
11 var separateInstance = new PartialPropertyDefaultProfile();
12 Assert.Equal("partial-default", separateInstance.InstanceValue);
13 Assert.NotSame(PartialPropertyDefaultProfile.Values, separateInstance.InstanceValues);
14 }
15
7 16 [Fact]
8 17 public async Task AllBuiltInFormatsRoundTrip()
9 18 {
@@ -33,8 +42,8 @@ public sealed class PersistenceTests
33 42 path => XmlRoundTripProfile.ProfilePath = path);
34 43 await AssertRoundTripAsync(
35 44 Path.Combine(directory, nameof(PartialPropertyProfile)),
36 value => PartialPropertyProfile.Current.Value = value,
37 () => PartialPropertyProfile.Current.Value,
45 value => PartialPropertyProfile.Value = value,
46 () => PartialPropertyProfile.Value,
38 47 () => PartialPropertyProfile.SaveProfileAsync(),
39 48 PartialPropertyProfile.LoadProfile,
40 49 path => PartialPropertyProfile.ProfilePath = path);
@@ -51,7 +60,7 @@ public sealed class PersistenceTests
51 60 XmlRoundTripProfile.Current.InstanceValue = "canonical";
52 61
53 62 var xml = XmlRoundTripProfile.ExportProfile();
54 PartialPropertyProfile.Current.Value = "partial-canonical";
63 PartialPropertyProfile.Value = "partial-canonical";
55 64 var partialXml = PartialPropertyProfile.ExportProfile();
56 65
57 66 Assert.Contains("<Value>canonical</Value>", xml);
@@ -69,7 +78,7 @@ public sealed class PersistenceTests
69 78 PartialPropertyProfile.ProfilePath = Path.Combine(directory, nameof(PartialPropertyProfile));
70 79 PartialPropertyProfile.Current.AutoSaveDelay = TimeSpan.FromMilliseconds(10);
71 80
72 PartialPropertyProfile.Current.Value = "auto-saved";
81 PartialPropertyProfile.Value = "auto-saved";
73 82 await PartialPropertyProfile.FlushAsync();
74 83
75 84 var xml = await File.ReadAllTextAsync(PartialPropertyProfile.ProfilePath + ".xml");
Modified XFEExtension.NetCore.AutoConfig/XFEExtension.NetCore.AutoConfig.csproj +1 -1
@@ -31,7 +31,7 @@
31 31 - 明确 Native AOT/Trim 支持边界并标注反射序列化入口
32 32 </PackageReleaseNotes>
33 33 <PackageLicenseFile>LICENSE.txt</PackageLicenseFile>
34 <Version>4.1.1</Version>
34 <Version>4.1.2</Version>
35 35 <GenerateDocumentationFile>True</GenerateDocumentationFile>
36 36 </PropertyGroup>
37 37