返回提交历史
Modified
README.md
+11
-12
Modified
README.zh-CN.md
+11
-12
Modified
XFEExtension.NetCore.AutoConfig.Analyzer/Diagnostics/AutoConfigDiagnostics.cs
+2
-2
Modified
XFEExtension.NetCore.AutoConfig.Analyzer/Generator/ProfilePropertyAutoGenerator.cs
+57
-25
Modified
XFEExtension.NetCore.AutoConfig.CodeFix/AutoConfigCodeFixProvider.cs
+1
-51
Modified
XFEExtension.NetCore.AutoConfig.Tests/GeneratorAndCodeFixTests.cs
+13
-10
Modified
XFEExtension.NetCore.AutoConfig.Tests/PersistenceProfiles.cs
+11
-1
Modified
XFEExtension.NetCore.AutoConfig.Tests/PersistenceTests.cs
+13
-4
Modified
XFEExtension.NetCore.AutoConfig/XFEExtension.NetCore.AutoConfig.csproj
+1
-1
XFEstudio/XFEExtension.NetCore.AutoConfig
调整 ProfileProperty 为静态 partial 属性,完善序列化支持
将 .NET 10/C# 14 的 ProfileProperty 部分属性由实例 partial 属性调整为静态 partial 属性,保留静态访问方式。生成器为每个静态 partial 属性自动生成实例桥接属性 InstanceXxx,仅供序列化器使用,保证初始值同步。修正 XML/JSON 持久化时元素/属性名为声明名。分析器和代码修复工具同步升级,自动迁移相关引用。文档和测试同步更新,版本号提升至 4.1.2。
782b820
代码差异
9 个文件
+120
-118
@@ -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
@@ -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
@@ -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
@@ -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
}
@@ -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,
@@ -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]
@@ -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
{
@@ -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");
@@ -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