返回提交历史
Modified
README.md
+12
-11
Modified
README.zh-CN.md
+12
-11
Modified
XFEExtension.NetCore.AutoConfig.Analyzer/Diagnostics/AutoConfigDiagnostics.cs
+7
-7
Modified
XFEExtension.NetCore.AutoConfig.Analyzer/Generator/ProfilePropertyAutoGenerator.cs
+85
-24
Modified
XFEExtension.NetCore.AutoConfig.CodeFix/AutoConfigCodeFixProvider.cs
+80
-27
Modified
XFEExtension.NetCore.AutoConfig.Tests/GeneratorAndCodeFixTests.cs
+69
-15
Modified
XFEExtension.NetCore.AutoConfig.Tests/PersistenceProfiles.cs
+1
-1
Modified
XFEExtension.NetCore.AutoConfig.Tests/PersistenceTests.cs
+25
-3
Modified
XFEExtension.NetCore.AutoConfig/XFEExtension.NetCore.AutoConfig.csproj
+1
-1
XFEstudio/XFEExtension.NetCore.AutoConfig
优化 ProfileProperty 部分属性支持及生成器逻辑
支持自然名称声明部分属性,持久化名与属性一致。生成器不再生成同名静态门面属性,需通过 Current 实例访问。CodeFix 自动重写静态访问为 Current.Xxx。支持自定义持久化名称,完善注册、序列化、集合绑定和自动保存逻辑。同步更新测试用例和文档,版本号升至 4.1.1。
53a5aca
代码差异
9 个文件
+292
-100
@@ -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, and collection binding. The existing static `Name` facade is still generated, so profile call sites do not change:
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:
57
57
58
58
```csharp
59
59
[AutoLoadProfile]
60
60
partial class SystemProfile : XFEProfile
61
61
{
62
62
[ProfileProperty]
63
public partial string InstanceName { get; set; } = string.Empty;
63
public partial string Name { get; set; } = string.Empty;
64
64
65
65
[ProfileProperty]
66
public partial int InstanceAge { get; set; }
66
public partial int Age { get; set; }
67
67
}
68
68
69
SystemProfile.Name = "Test";
70
Console.WriteLine(SystemProfile.Age);
69
SystemProfile.Current.Name = "Test";
70
Console.WriteLine(SystemProfile.Current.Age);
71
71
```
72
72
73
Partial profile properties must be named `InstanceXxx`; the persisted name and static facade remain `Xxx`. For example, `[ProfileProperty("DisplayName")]` maps to `InstanceDisplayName`, static `DisplayName`, and the XML element `<DisplayName>`.
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.
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 and 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, 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:
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 InstanceName { get; set; } = "Guest";
84
public 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>`) even though the generated C# instance property remains `InstanceValue`.
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.
135
135
136
136
```csharp
137
137
[AutoLoadProfile]
@@ -482,8 +482,9 @@ 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` (per profile member) | `static T` | Auto-generated static property; saves on set |
486
| `InstanceXxx` (per profile member) | `T` (instance) | Corresponding instance property; may be user-declared as a partial property on .NET 10 |
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 |
487
488
| `GetXxxProperty()` | `static partial void` | Invoked when the property is read |
488
489
| `SetXxxProperty(ref T)` | `static partial void` | Invoked when the property is written |
489
490
@@ -53,26 +53,26 @@ class Program
53
53
54
54
### .NET 10 部分属性
55
55
56
目标项目为 .NET 10 或更高版本并使用 C# 14 时,可以直接声明由生成器实现的部分实例属性。生成器会用 `field` 实现后备存储、并发锁与集合绑定,同时仍生成原有的静态 `Name` 属性,因此调用配置的方式不变:
56
目标项目为 .NET 10 或更高版本并使用 C# 14 时,可以直接声明由生成器实现的部分实例属性。生成器会用 `field` 实现后备存储、并发锁、集合绑定与自动保存:
57
57
58
58
```csharp
59
59
[AutoLoadProfile]
60
60
partial class SystemProfile : XFEProfile
61
61
{
62
62
[ProfileProperty]
63
public partial string InstanceName { get; set; } = string.Empty;
63
public partial string Name { get; set; } = string.Empty;
64
64
65
65
[ProfileProperty]
66
public partial int InstanceAge { get; set; }
66
public partial int Age { get; set; }
67
67
}
68
68
69
SystemProfile.Name = "Test";
70
Console.WriteLine(SystemProfile.Age);
69
SystemProfile.Current.Name = "Test";
70
Console.WriteLine(SystemProfile.Current.Age);
71
71
```
72
72
73
部分属性必须命名为 `InstanceXxx`;持久化名称和静态属性名仍为 `Xxx`。例如 `[ProfileProperty("DisplayName")]` 对应 `InstanceDisplayName`、静态 `DisplayName` 和 XML 节点 `<DisplayName>`。
73
部分属性直接使用自然名称 `Xxx`,不需要 `Instance` 前缀,持久化名称同样为 `Xxx`。由于 C# 不允许同一个类型同时声明实例 `Name` 和静态 `Name`,部分属性模式不再生成同名静态门面,应通过 `SystemProfile.Current.Name` 访问。字段模式仍保留原有的静态 `Name` 和实例 `InstanceName` API。
74
74
75
旧的字段写法继续受到支持。对于目标为 .NET 10+ 且启用 C# 14 的项目,分析器会以 `XFE0003` 提示可升级字段,并提供“转换为 .NET 10 部分配置属性”代码修复。修复会保留初始值、更新字段引用和 get/set 特性中的字段名;原字段上的其他特性会改为 `field:` 目标。手动升级时可按以下方式转换:
75
旧的字段写法继续受到支持。对于目标为 .NET 10+ 且启用 C# 14 的项目,分析器会以 `XFE0003` 提示可升级字段,并提供“转换为 .NET 10 部分配置属性”代码修复。修复会保留初始值、更新字段引用,把已有的静态访问改为 `Current.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 InstanceName { get; set; } = "Guest";
84
public 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`。
134
XML 节点会使用配置属性名称(例如 `<Value>`)。字段模式生成的 C# 实例属性为 `InstanceValue`,部分属性模式则直接使用用户声明的 `Value`。
135
135
136
136
```csharp
137
137
[AutoLoadProfile]
@@ -475,8 +475,9 @@ 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`(实例)| 对应的实例属性;.NET 10 下可由用户声明为部分属性 |
478
| `Xxx`(字段模式)| `static T` | 自动生成的静态属性,读写时自动持久化 |
479
| `InstanceXxx`(字段模式)| `T`(实例)| 字段对应的实例属性 |
480
| `Current.Xxx`(部分属性模式)| `T`(实例)| 用户声明的自然名称部分属性,读写时自动持久化 |
480
481
| `GetXxxProperty()` | `static partial void` | get 钩子分部方法 |
481
482
| `SetXxxProperty(ref T)` | `static partial void` | set 钩子分部方法 |
482
483
@@ -15,6 +15,7 @@ public sealed class AutoConfigDiagnostics : DiagnosticAnalyzer
15
15
private const string AddGetAttributeName = "XFEExtension.NetCore.AutoConfig.ProfilePropertyAddGetAttribute";
16
16
private const string AddSetAttributeName = "XFEExtension.NetCore.AutoConfig.ProfilePropertyAddSetAttribute";
17
17
private const string ProfilePropertyAttributeName = "XFEExtension.NetCore.AutoConfig.ProfilePropertyAttribute";
18
private const string GeneratedProfilePropertyAttributeName = "XFEExtension.NetCore.AutoConfig.ProfileFieldAutoGenerateAttribute";
18
19
private const string ProfileBaseTypeName = "XFEExtension.NetCore.AutoConfig.XFEProfile";
19
20
private const string TargetFrameworkAttributeName = "System.Runtime.Versioning.TargetFrameworkAttribute";
20
21
@@ -102,7 +103,7 @@ public sealed class AutoConfigDiagnostics : DiagnosticAnalyzer
102
103
var setAttributes = property.GetAttributes().Where(static attribute => attribute.AttributeClass?.ToDisplayString() == AddSetAttributeName).ToArray();
103
104
if (setAttributes.Length == 0)
104
105
return;
105
var assignmentPattern = $@"\b{Regex.Escape(property.Name)}\s*=\s*value\b";
106
var assignmentPattern = @"\bfield\s*=\s*value\b";
106
107
if (!setAttributes.Select(GetCode).Any(code => code is not null && Regex.IsMatch(code, assignmentPattern)))
107
108
context.ReportDiagnostic(Diagnostic.Create(AddSetNoSetResultWarning, GetAttributeLocation(setAttributes[setAttributes.Length - 1], declaration), property.Name));
108
109
}
@@ -130,19 +131,18 @@ public sealed class AutoConfigDiagnostics : DiagnosticAnalyzer
130
131
|| SyntaxFacts.GetContextualKeywordKind(propertyName) != SyntaxKind.None)
131
132
return;
132
133
133
var instancePropertyName = "Instance" + propertyName;
134
if (HasMemberConflict(field, propertyName) || HasMemberConflict(field, instancePropertyName))
134
if (HasMemberConflict(field, propertyName))
135
135
return;
136
136
137
137
var properties = ImmutableDictionary<string, string?>.Empty
138
.Add("PropertyName", propertyName)
139
.Add("InstancePropertyName", instancePropertyName);
140
context.ReportDiagnostic(Diagnostic.Create(FieldCanUsePartialProperty, variable.Identifier.GetLocation(), properties, field.Name, instancePropertyName));
138
.Add("PropertyName", propertyName);
139
context.ReportDiagnostic(Diagnostic.Create(FieldCanUsePartialProperty, variable.Identifier.GetLocation(), properties, field.Name, propertyName));
141
140
}
142
141
143
142
private static bool HasMemberConflict(IFieldSymbol field, string memberName) => field.ContainingType
144
143
.GetMembers(memberName)
145
.Any(member => !SymbolEqualityComparer.Default.Equals(member, field));
144
.Any(member => !SymbolEqualityComparer.Default.Equals(member, field)
145
&& !member.GetAttributes().Any(static attribute => attribute.AttributeClass?.ToDisplayString() == GeneratedProfilePropertyAttributeName));
146
146
147
147
private static bool SupportsNet10PartialProperties(SyntaxNodeAnalysisContext context)
148
148
{
@@ -58,7 +58,7 @@ public sealed class ProfilePropertyAutoGenerator : IIncrementalGenerator
58
58
DiagnosticSeverity.Error, true);
59
59
60
60
private static readonly DiagnosticDescriptor UnsupportedPartialProperty = new(
61
"XFE1009", "不支持的部分配置属性", "属性“{0}”必须使用 C# 14 的 public partial InstanceXxx {{ get; set; }} 声明,并且不能是 static、required、virtual 或显式接口实现", "XFEExtension.NetCore.AutoConfig.Generator",
61
"XFE1009", "不支持的部分配置属性", "属性“{0}”必须使用 C# 14 的 public partial Xxx {{ get; set; }} 声明,并且不能是 static、required、virtual 或显式接口实现", "XFEExtension.NetCore.AutoConfig.Generator",
62
62
DiagnosticSeverity.Error, true);
63
63
64
64
public void Initialize(IncrementalGeneratorInitializationContext context)
@@ -203,10 +203,24 @@ public sealed class ProfilePropertyAutoGenerator : IIncrementalGenerator
203
203
}
204
204
205
205
var invalidModels = new HashSet<ProfileMemberModel>();
206
var profileNames = new Dictionary<string, ProfileMemberModel>(StringComparer.Ordinal);
206
207
var generatedNames = new Dictionary<string, ProfileMemberModel>(StringComparer.Ordinal);
207
208
foreach (var model in models)
208
209
{
209
var names = new[] { model.PropertyName, "Instance" + model.PropertyName, "Get" + model.PropertyName + "Property", "Set" + model.PropertyName + "Property" };
210
if (profileNames.TryGetValue(model.PropertyName, out var existingProfileModel))
211
{
212
context.ReportDiagnostic(Diagnostic.Create(GeneratedPropertyConflict, GetLocation(model.Member), model.Member.Name, model.PropertyName));
213
invalidModels.Add(model);
214
invalidModels.Add(existingProfileModel);
215
}
216
else
217
{
218
profileNames.Add(model.PropertyName, model);
219
}
220
221
var names = model.IsPartialProperty
222
? new[] { "Get" + model.PropertyName + "Property", "Set" + model.PropertyName + "Property" }
223
: new[] { model.PropertyName, "Instance" + model.PropertyName, "Get" + model.PropertyName + "Property", "Set" + model.PropertyName + "Property" };
210
224
foreach (var name in names)
211
225
{
212
226
if (generatedNames.TryGetValue(name, out var existingModel))
@@ -222,10 +236,7 @@ public sealed class ProfilePropertyAutoGenerator : IIncrementalGenerator
222
236
223
237
if ((name == "Get" + model.PropertyName + "Property" || name == "Set" + model.PropertyName + "Property") && IsPartialHook(type, name))
224
238
continue;
225
var conflictingMember = type.GetMembers(name).FirstOrDefault(member =>
226
!(model.IsPartialProperty
227
&& name == model.StorageMemberName
228
&& SymbolEqualityComparer.Default.Equals(member, model.Member)));
239
var conflictingMember = type.GetMembers(name).FirstOrDefault();
229
240
if (conflictingMember is null)
230
241
continue;
231
242
context.ReportDiagnostic(Diagnostic.Create(GeneratedPropertyConflict, GetLocation(model.Member), model.Member.Name, name));
@@ -254,7 +265,6 @@ public sealed class ProfilePropertyAutoGenerator : IIncrementalGenerator
254
265
context.ReportDiagnostic(Diagnostic.Create(GeneratedPropertyConflict, GetLocation(property), property.Name, propertyName));
255
266
return null;
256
267
}
257
var expectedInstanceName = "Instance" + propertyName;
258
268
var isSupported = declaration is not null
259
269
&& parseOptions is not null
260
270
&& parseOptions.LanguageVersion >= LanguageVersion.CSharp14
@@ -273,8 +283,7 @@ public sealed class ProfilePropertyAutoGenerator : IIncrementalGenerator
273
283
&& property.GetMethod is not null
274
284
&& property.SetMethod is not null
275
285
&& !property.SetMethod.IsInitOnly
276
&& hasSupportedAccessors
277
&& property.Name == expectedInstanceName;
286
&& hasSupportedAccessors;
278
287
279
288
if (!isSupported)
280
289
{
@@ -360,11 +369,14 @@ public sealed class ProfilePropertyAutoGenerator : IIncrementalGenerator
360
369
{
361
370
var storageMemberName = EscapeIdentifier(member.StorageMemberName);
362
371
var propertyName = EscapeIdentifier(member.PropertyName);
372
var propertyKeyExpression = member.IsPartialProperty
373
? SyntaxFactory.Literal(member.PropertyName).ToFullString()
374
: "nameof(" + propertyName + ")";
363
375
var declarationTypeDisplay = member.Type.ToDisplayString(DeclarationTypeDisplayFormat);
364
376
var runtimeTypeDisplay = member.Type.ToDisplayString(RuntimeTypeDisplayFormat);
365
builder.Append(" this.PropertyInfoDictionary[nameof(").Append(propertyName).Append(")] = typeof(").Append(runtimeTypeDisplay).AppendLine(");");
366
builder.Append(" this.PropertySetFuncDictionary[nameof(").Append(propertyName).Append(")] = value => this.").Append(storageMemberName).Append(" = (").Append(declarationTypeDisplay).AppendLine(")value!;");
367
builder.Append(" this.PropertyGetFuncDictionary[nameof(").Append(propertyName).Append(")] = () => this.").Append(storageMemberName).AppendLine(";");
377
builder.Append(" this.PropertyInfoDictionary[").Append(propertyKeyExpression).Append("] = typeof(").Append(runtimeTypeDisplay).AppendLine(");");
378
builder.Append(" this.PropertySetFuncDictionary[").Append(propertyKeyExpression).Append("] = value => this.").Append(storageMemberName).Append(" = (").Append(declarationTypeDisplay).AppendLine(")value!;");
379
builder.Append(" this.PropertyGetFuncDictionary[").Append(propertyKeyExpression).Append("] = () => this.").Append(storageMemberName).AppendLine(";");
368
380
}
369
381
builder.AppendLine(" this.__BindProfileOwnedCollections();");
370
382
builder.AppendLine(" }");
@@ -477,6 +489,13 @@ public sealed class ProfilePropertyAutoGenerator : IIncrementalGenerator
477
489
builder.Append(" static partial void ").Append(getMethodName).AppendLine("();");
478
490
builder.Append(" static partial void ").Append(setMethodName).Append("(ref ").Append(typeDisplay).AppendLine(" value);");
479
491
builder.AppendLine();
492
493
if (member.IsPartialProperty)
494
{
495
AppendPartialProfileProperty(builder, member, storageMemberName, getMethodName, setMethodName, typeDisplay, xmlElementNameLiteral);
496
return;
497
}
498
480
499
builder.AppendLine(" [global::XFEExtension.NetCore.AutoConfig.ProfileFieldAutoGenerateAttribute]");
481
500
builder.Append(" public static ").Append(typeDisplay).Append(' ').Append(propertyName).AppendLine();
482
501
builder.AppendLine(" {");
@@ -502,7 +521,7 @@ public sealed class ProfilePropertyAutoGenerator : IIncrementalGenerator
502
521
builder.Append(" __current.").Append(storageMemberName).AppendLine(" = value;");
503
522
else
504
523
AppendStatements(builder, member.SetStatements, 16);
505
if (IsProfileOwnedCollection(member.Type) && !member.IsPartialProperty)
524
if (IsProfileOwnedCollection(member.Type))
506
525
builder.AppendLine(" __current.__BindProfileOwnedCollections();");
507
526
builder.AppendLine(" __current.InstanceRequestSaveProfile();");
508
527
builder.AppendLine(" }");
@@ -511,23 +530,65 @@ public sealed class ProfilePropertyAutoGenerator : IIncrementalGenerator
511
530
builder.AppendLine();
512
531
builder.AppendLine(" [global::XFEExtension.NetCore.AutoConfig.ProfileFieldAutoGenerateAttribute]");
513
532
builder.Append(" [global::System.Xml.Serialization.XmlElementAttribute(").Append(xmlElementNameLiteral).AppendLine(")]");
514
builder.Append(" public ");
515
if (member.IsPartialProperty)
516
builder.Append("partial ");
517
builder.Append(typeDisplay).Append(' ').Append(instancePropertyName).AppendLine();
533
builder.Append(" public ").Append(typeDisplay).Append(' ').Append(instancePropertyName).AppendLine();
518
534
builder.AppendLine(" {");
519
builder.Append(" get { lock (ProfileSyncRoot) return ");
520
builder.Append(member.IsPartialProperty ? "field" : "this." + storageMemberName).AppendLine("; }");
535
builder.Append(" get { lock (ProfileSyncRoot) return this.").Append(storageMemberName).AppendLine("; }");
521
536
if (IsProfileOwnedCollection(member.Type))
522
537
{
523
builder.Append(" set { lock (ProfileSyncRoot) { ");
524
builder.Append(member.IsPartialProperty ? "field" : "this." + storageMemberName).AppendLine(" = value; this.__BindProfileOwnedCollections(); } }");
538
builder.Append(" set { lock (ProfileSyncRoot) { this.").Append(storageMemberName).AppendLine(" = value; this.__BindProfileOwnedCollections(); } }");
525
539
}
526
540
else
527
541
{
528
builder.Append(" set { lock (ProfileSyncRoot) ");
529
builder.Append(member.IsPartialProperty ? "field" : "this." + storageMemberName).AppendLine(" = value; }");
542
builder.Append(" set { lock (ProfileSyncRoot) this.").Append(storageMemberName).AppendLine(" = value; }");
543
}
544
builder.AppendLine(" }");
545
builder.AppendLine();
546
}
547
548
private static void AppendPartialProfileProperty(
549
StringBuilder builder,
550
ProfileMemberModel member,
551
string propertyName,
552
string getMethodName,
553
string setMethodName,
554
string typeDisplay,
555
string xmlElementNameLiteral)
556
{
557
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(")]");
530
563
}
564
builder.Append(" public partial ").Append(typeDisplay).Append(' ').Append(propertyName).AppendLine();
565
builder.AppendLine(" {");
566
builder.AppendLine(" get");
567
builder.AppendLine(" {");
568
builder.AppendLine(" lock (ProfileSyncRoot)");
569
builder.AppendLine(" {");
570
builder.Append(" ").Append(getMethodName).AppendLine("();");
571
if (member.GetStatements.Length == 0)
572
builder.AppendLine(" return field;");
573
else
574
AppendStatements(builder, member.GetStatements, 16);
575
builder.AppendLine(" }");
576
builder.AppendLine(" }");
577
builder.AppendLine(" set");
578
builder.AppendLine(" {");
579
builder.AppendLine(" lock (ProfileSyncRoot)");
580
builder.AppendLine(" {");
581
builder.Append(" ").Append(setMethodName).AppendLine("(ref value);");
582
if (member.SetStatements.Length == 0)
583
builder.AppendLine(" field = value;");
584
else
585
AppendStatements(builder, member.SetStatements, 16);
586
if (IsProfileOwnedCollection(member.Type))
587
builder.AppendLine(" this.__BindProfileOwnedCollections();");
588
builder.AppendLine(" if (global::System.Object.ReferenceEquals(this, __current))");
589
builder.AppendLine(" this.InstanceRequestSaveProfile();");
590
builder.AppendLine(" }");
591
builder.AppendLine(" }");
531
592
builder.AppendLine(" }");
532
593
builder.AppendLine();
533
594
}
@@ -559,7 +620,7 @@ public sealed class ProfilePropertyAutoGenerator : IIncrementalGenerator
559
620
return explicitName!;
560
621
561
622
if (member is IPropertySymbol property)
562
return property.Name.StartsWith("Instance", StringComparison.Ordinal) ? property.Name.Substring("Instance".Length) : property.Name;
623
return property.Name;
563
624
564
625
var fieldName = member.Name.StartsWith("_", StringComparison.Ordinal) ? member.Name.Substring(1) : member.Name;
565
626
if (fieldName.Length == 0)
@@ -22,6 +22,7 @@ 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";
25
26
private const string AddGetAttributeName = "XFEExtension.NetCore.AutoConfig.ProfilePropertyAddGetAttribute";
26
27
private const string AddSetAttributeName = "XFEExtension.NetCore.AutoConfig.ProfilePropertyAddSetAttribute";
27
28
@@ -46,13 +47,13 @@ public sealed class AutoConfigCodeFixProvider : CodeFixProvider
46
47
diagnostic);
47
48
}
48
49
else if (diagnostic.Id == AutoConfigDiagnostics.FieldCanUsePartialPropertyId
49
&& diagnostic.Properties.TryGetValue("InstancePropertyName", out var instancePropertyName)
50
&& !string.IsNullOrWhiteSpace(instancePropertyName))
50
&& diagnostic.Properties.TryGetValue("PropertyName", out var propertyName)
51
&& !string.IsNullOrWhiteSpace(propertyName))
51
52
{
52
53
context.RegisterCodeFix(
53
54
CodeAction.Create(
54
55
"转换为 .NET 10 部分配置属性",
55
cancellationToken => ConvertToPartialPropertyAsync(context.Document, diagnostic.Location.SourceSpan, instancePropertyName!, cancellationToken),
56
cancellationToken => ConvertToPartialPropertyAsync(context.Document, diagnostic.Location.SourceSpan, propertyName!, cancellationToken),
56
57
"转换为部分配置属性"),
57
58
diagnostic);
58
59
}
@@ -65,18 +66,26 @@ public sealed class AutoConfigCodeFixProvider : CodeFixProvider
65
66
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
66
67
if (root is null)
67
68
return document;
68
var fieldDeclaration = root.FindToken(sourceSpan.Start).Parent?.AncestorsAndSelf().OfType<FieldDeclarationSyntax>().FirstOrDefault();
69
if (fieldDeclaration is null)
69
var diagnosticNode = root.FindToken(sourceSpan.Start).Parent;
70
var fieldDeclaration = diagnosticNode?.AncestorsAndSelf().OfType<FieldDeclarationSyntax>().FirstOrDefault();
71
var propertyDeclaration = diagnosticNode?.AncestorsAndSelf().OfType<PropertyDeclarationSyntax>().FirstOrDefault();
72
if (fieldDeclaration is null && propertyDeclaration is null)
70
73
return document;
71
var fieldName = fieldDeclaration.Declaration.Variables.First().Identifier.ValueText;
72
var code = string.Format(System.Globalization.CultureInfo.InvariantCulture, codeFormat, fieldName);
74
var code = propertyDeclaration is null
75
? string.Format(System.Globalization.CultureInfo.InvariantCulture, codeFormat, fieldDeclaration!.Declaration.Variables.First().Identifier.ValueText)
76
: attributeName == AddGetAttributeName || attributeName == "ProfilePropertyAddGet"
77
? "return field"
78
: "field = value";
73
79
var newAttribute = SyntaxFactory.Attribute(SyntaxFactory.ParseName(attributeName))
74
80
.AddArgumentListArguments(SyntaxFactory.AttributeArgument(SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(code))));
75
var newRoot = root.ReplaceNode(fieldDeclaration, fieldDeclaration.AddAttributeLists(SyntaxFactory.AttributeList(SyntaxFactory.SingletonSeparatedList(newAttribute))));
81
var attributeList = SyntaxFactory.AttributeList(SyntaxFactory.SingletonSeparatedList(newAttribute));
82
var newRoot = fieldDeclaration is not null
83
? root.ReplaceNode(fieldDeclaration, fieldDeclaration.AddAttributeLists(attributeList))
84
: root.ReplaceNode(propertyDeclaration!, propertyDeclaration!.AddAttributeLists(attributeList));
76
85
return document.WithSyntaxRoot(newRoot);
77
86
}
78
87
79
private static async Task<Solution> ConvertToPartialPropertyAsync(Document document, TextSpan sourceSpan, string instancePropertyName, CancellationToken cancellationToken)
88
private static async Task<Solution> ConvertToPartialPropertyAsync(Document document, TextSpan sourceSpan, string propertyName, CancellationToken cancellationToken)
80
89
{
81
90
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
82
91
var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
@@ -91,6 +100,10 @@ public sealed class AutoConfigCodeFixProvider : CodeFixProvider
91
100
return document.Project.Solution;
92
101
93
102
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));
94
107
var profileAttributeNames = new HashSet<string>(StringComparer.Ordinal);
95
108
var getHookAttributeNames = new HashSet<string>(StringComparer.Ordinal);
96
109
var setHookAttributeNames = new HashSet<string>(StringComparer.Ordinal);
@@ -107,15 +120,18 @@ public sealed class AutoConfigCodeFixProvider : CodeFixProvider
107
120
setHookAttributeNames.Add(sourceName);
108
121
}
109
122
var solution = document.Project.Solution;
123
var declarationAnnotation = new SyntaxAnnotation();
124
solution = solution.WithDocumentSyntaxRoot(
125
document.Id,
126
root.ReplaceNode(fieldDeclaration, fieldDeclaration.WithAdditionalAnnotations(declarationAnnotation)));
127
if (generatedStaticFacade is not null)
128
solution = await RewriteStaticFacadeReferencesAsync(solution, generatedStaticFacade, cancellationToken).ConfigureAwait(false);
129
110
130
var referencedSymbols = await SymbolFinder.FindReferencesAsync(field, solution, cancellationToken).ConfigureAwait(false);
111
131
var referenceLocations = referencedSymbols
112
132
.SelectMany(static referencedSymbol => referencedSymbol.Locations)
113
133
.Where(static location => location.Location.IsInSource)
114
134
.GroupBy(static location => location.Document.Id);
115
var declarationAnnotation = new SyntaxAnnotation();
116
solution = solution.WithDocumentSyntaxRoot(
117
document.Id,
118
root.ReplaceNode(fieldDeclaration, fieldDeclaration.WithAdditionalAnnotations(declarationAnnotation)));
119
135
120
136
foreach (var documentLocations in referenceLocations)
121
137
{
@@ -130,7 +146,7 @@ public sealed class AutoConfigCodeFixProvider : CodeFixProvider
130
146
.ToArray();
131
147
if (tokens.Length == 0)
132
148
continue;
133
var renamedRoot = referenceRoot.ReplaceTokens(tokens, (_, rewritten) => SyntaxFactory.Identifier(rewritten.LeadingTrivia, instancePropertyName, rewritten.TrailingTrivia));
149
var renamedRoot = referenceRoot.ReplaceTokens(tokens, (_, rewritten) => SyntaxFactory.Identifier(rewritten.LeadingTrivia, propertyName, rewritten.TrailingTrivia));
134
150
solution = solution.WithDocumentSyntaxRoot(referenceDocument.Id, renamedRoot);
135
151
}
136
152
@@ -148,11 +164,11 @@ public sealed class AutoConfigCodeFixProvider : CodeFixProvider
148
164
var attributeLists = PreparePartialPropertyAttributes(
149
165
currentFieldDeclaration.AttributeLists,
150
166
originalFieldName,
151
instancePropertyName,
167
"field",
152
168
profileAttributeNames,
153
169
getHookAttributeNames,
154
170
setHookAttributeNames);
155
var partialProperty = SyntaxFactory.PropertyDeclaration(currentFieldDeclaration.Declaration.Type.WithoutTrivia(), SyntaxFactory.Identifier(instancePropertyName))
171
var partialProperty = SyntaxFactory.PropertyDeclaration(currentFieldDeclaration.Declaration.Type.WithoutTrivia(), SyntaxFactory.Identifier(propertyName))
156
172
.WithAttributeLists(attributeLists)
157
173
.WithModifiers(SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.PublicKeyword), SyntaxFactory.Token(SyntaxKind.PartialKeyword)))
158
174
.WithAccessorList(SyntaxFactory.AccessorList(SyntaxFactory.List([
@@ -168,10 +184,52 @@ public sealed class AutoConfigCodeFixProvider : CodeFixProvider
168
184
return solution.WithDocumentSyntaxRoot(declarationDocument.Id, updatedRoot);
169
185
}
170
186
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
171
229
private static SyntaxList<AttributeListSyntax> RewriteHookAttributeStrings(
172
230
SyntaxList<AttributeListSyntax> attributeLists,
173
231
string fieldName,
174
string instancePropertyName,
232
string backingFieldName,
175
233
HashSet<string> getHookAttributeNames,
176
234
HashSet<string> setHookAttributeNames)
177
235
{
@@ -189,16 +247,11 @@ public sealed class AutoConfigCodeFixProvider : CodeFixProvider
189
247
|| !literal.IsKind(SyntaxKind.StringLiteralExpression))
190
248
return attribute;
191
249
var rewrittenCode = literal.Token.ValueText;
250
var qualifiedFieldPattern = $@"(?:(?:\b@?[A-Za-z_][A-Za-z0-9_]*\s*\.\s*)*)(?:Current|this)\s*\.\s*{Regex.Escape(fieldName)}\b";
251
rewrittenCode = Regex.Replace(rewrittenCode, qualifiedFieldPattern, backingFieldName);
192
252
if (isGetHook || fieldName != "value")
193
253
{
194
rewrittenCode = Regex.Replace(rewrittenCode, $@"\b{Regex.Escape(fieldName)}\b", instancePropertyName);
195
}
196
else
197
{
198
rewrittenCode = Regex.Replace(
199
rewrittenCode,
200
$@"(?<memberAccess>\.\s*){Regex.Escape(fieldName)}\b",
201
"${memberAccess}" + instancePropertyName);
254
rewrittenCode = Regex.Replace(rewrittenCode, $@"\b{Regex.Escape(fieldName)}\b", backingFieldName);
202
255
}
203
256
var rewrittenLiteral = SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(rewrittenCode));
204
257
return attribute.ReplaceNode(literal, rewrittenLiteral);
@@ -208,12 +261,12 @@ public sealed class AutoConfigCodeFixProvider : CodeFixProvider
208
261
private static SyntaxList<AttributeListSyntax> PreparePartialPropertyAttributes(
209
262
SyntaxList<AttributeListSyntax> attributeLists,
210
263
string fieldName,
211
string instancePropertyName,
264
string backingFieldName,
212
265
HashSet<string> profileAttributeNames,
213
266
HashSet<string> getHookAttributeNames,
214
267
HashSet<string> setHookAttributeNames)
215
268
{
216
var rewrittenLists = RewriteHookAttributeStrings(attributeLists, fieldName, instancePropertyName, getHookAttributeNames, setHookAttributeNames);
269
var rewrittenLists = RewriteHookAttributeStrings(attributeLists, fieldName, backingFieldName, getHookAttributeNames, setHookAttributeNames);
217
270
var result = new List<AttributeListSyntax>();
218
271
foreach (var attributeList in rewrittenLists)
219
272
{
@@ -80,16 +80,40 @@ public sealed class GeneratorAndCodeFixTests
80
80
public partial class Settings : XFEProfile
81
81
{
82
82
[ProfileProperty]
83
public partial string InstanceName { get; set; } = "default";
83
public 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.Contains("public static string Name", generatedSource);
91
Assert.Contains("public partial string InstanceName", generatedSource);
92
Assert.Contains("get { lock (ProfileSyncRoot) return field; }", generatedSource);
90
Assert.DoesNotContain("public static string Name", generatedSource);
91
Assert.Contains("public partial string Name", generatedSource);
92
Assert.Contains("return field;", generatedSource);
93
Assert.Contains("this.InstanceRequestSaveProfile();", generatedSource);
94
Assert.DoesNotContain(outputCompilation.GetDiagnostics(), diagnostic => diagnostic.Severity == DiagnosticSeverity.Error);
95
}
96
97
[Fact]
98
public void GeneratorSupportsCustomPersistedNameOnNaturalPartialProperty()
99
{
100
const string source = """
101
using XFEExtension.NetCore.AutoConfig;
102
[AutoLoadProfile(false)]
103
public partial class Settings : XFEProfile
104
{
105
[ProfileProperty("StoredName")]
106
public partial string Name { get; set; } = "default";
107
}
108
""";
109
110
var (result, outputCompilation) = RunGenerator(source);
111
var generatedSource = result.Results.Single().GeneratedSources.Single().SourceText.ToString();
112
113
Assert.Contains("XmlElementAttribute(\"StoredName\")", generatedSource);
114
Assert.Contains("JsonPropertyNameAttribute(\"StoredName\")", generatedSource);
115
Assert.Contains("PropertyInfoDictionary[\"StoredName\"]", generatedSource);
116
Assert.Contains("public partial string Name", generatedSource);
93
117
Assert.DoesNotContain(outputCompilation.GetDiagnostics(), diagnostic => diagnostic.Severity == DiagnosticSeverity.Error);
94
118
}
95
119
@@ -100,7 +124,7 @@ public sealed class GeneratorAndCodeFixTests
100
124
[InlineData("public partial class Settings : XFEProfile { public Settings(int value) { } [ProfileProperty] private int value; }", "XFE1005")]
101
125
[InlineData("public partial class Settings : XFEProfile { public static Settings Current { get; set; } = null!; [ProfileProperty] private int value; }", "XFE1006")]
102
126
[InlineData("public partial class Settings { [ProfileProperty] private int value; }", "XFE1008")]
103
[InlineData("public partial class Settings : XFEProfile { [ProfileProperty] public partial int Value { get; set; } }", "XFE1009")]
127
[InlineData("public partial class Settings : XFEProfile { [ProfileProperty] public partial int Value { get; init; } }", "XFE1009")]
104
128
public void GeneratorReportsActionableUsageDiagnostics(string declaration, string diagnosticId)
105
129
{
106
130
var source = "using XFEExtension.NetCore.AutoConfig;\n" + declaration;
@@ -210,12 +234,27 @@ public sealed class GeneratorAndCodeFixTests
210
234
using var workspace = new AdhocWorkspace();
211
235
var projectId = ProjectId.CreateNewId();
212
236
var documentId = DocumentId.CreateNewId(projectId);
237
var generatedDocumentId = DocumentId.CreateNewId(projectId);
238
var consumerDocumentId = DocumentId.CreateNewId(projectId);
213
239
var solution = workspace.CurrentSolution
214
240
.AddProject(projectId, "PartialPropertyCodeFixTest", "PartialPropertyCodeFixTest", LanguageNames.CSharp)
215
241
.WithProjectParseOptions(projectId, new CSharpParseOptions(LanguageVersion.Preview))
216
242
.WithProjectCompilationOptions(projectId, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, nullableContextOptions: NullableContextOptions.Enable))
217
243
.AddMetadataReferences(projectId, GetMetadataReferences())
218
.AddDocument(documentId, "Settings.cs", SourceText.From(source));
244
.AddDocument(documentId, "Settings.cs", SourceText.From(source))
245
.AddDocument(generatedDocumentId, "Settings.Generated.cs", SourceText.From("""
246
public partial class Settings
247
{
248
[XFEExtension.NetCore.AutoConfig.ProfileFieldAutoGenerate]
249
public static string Value { get; set; } = string.Empty;
250
}
251
"""))
252
.AddDocument(consumerDocumentId, "Consumer.cs", SourceText.From("""
253
public static class Consumer
254
{
255
public static string Read() => Settings.Value;
256
}
257
"""));
219
258
Assert.True(workspace.TryApplyChanges(solution));
220
259
var document = workspace.CurrentSolution.GetDocument(documentId)!;
221
260
var compilation = (CSharpCompilation)(await document.Project.GetCompilationAsync())!;
@@ -231,13 +270,17 @@ public sealed class GeneratorAndCodeFixTests
231
270
var changedDocument = changedSolution.GetDocument(documentId)!;
232
271
var changedText = (await changedDocument.GetTextAsync()).ToString();
233
272
234
Assert.Contains("public partial string InstanceValue", changedText);
273
Assert.Contains("public partial string Value", changedText);
235
274
Assert.Contains("field:", changedText);
236
275
Assert.Contains("System.NonSerialized", changedText);
237
276
Assert.Contains("[Profile]", changedText);
238
Assert.Contains("Current.InstanceValue = value", changedText);
277
Assert.Contains("field = value", changedText);
239
278
Assert.Contains("= \"default\";", changedText);
240
Assert.Contains("ReadValue() => InstanceValue", changedText);
279
Assert.Contains("ReadValue() => Value", changedText);
280
var consumerText = (await changedSolution.GetDocument(consumerDocumentId)!.GetTextAsync()).ToString();
281
Assert.Contains("Settings.Current.Value", consumerText);
282
changedSolution = changedSolution.RemoveDocument(generatedDocumentId);
283
changedDocument = changedSolution.GetDocument(documentId)!;
241
284
var changedCompilation = (CSharpCompilation)(await changedDocument.Project.GetCompilationAsync())!;
242
285
GeneratorDriver driver = CSharpGeneratorDriver.Create(
243
286
generators: [new ProfilePropertyAutoGenerator().AsSourceGenerator()],
@@ -247,17 +290,28 @@ public sealed class GeneratorAndCodeFixTests
247
290
}
248
291
249
292
[Theory]
250
[InlineData("ProfilePropertyAddGet", "System.Console.WriteLine(1)", AutoConfigDiagnostics.AddGetNoResultErrorId, "return Current.value")]
251
[InlineData("ProfilePropertyAddSet", "System.Console.WriteLine(value)", AutoConfigDiagnostics.AddSetNoSetResultWarningId, "Current.value = value")]
252
public async Task CodeFixAddsRequiredHookStatement(string attributeName, string existingCode, string diagnosticId, string expectedCode)
293
[InlineData("ProfilePropertyAddGet", "System.Console.WriteLine(1)", AutoConfigDiagnostics.AddGetNoResultErrorId, "return Current.value", false)]
294
[InlineData("ProfilePropertyAddSet", "System.Console.WriteLine(value)", AutoConfigDiagnostics.AddSetNoSetResultWarningId, "Current.value = value", false)]
295
[InlineData("ProfilePropertyAddGet", "System.Console.WriteLine(1)", AutoConfigDiagnostics.AddGetNoResultErrorId, "return field", true)]
296
[InlineData("ProfilePropertyAddSet", "System.Console.WriteLine(value)", AutoConfigDiagnostics.AddSetNoSetResultWarningId, "field = value", true)]
297
public async Task CodeFixAddsRequiredHookStatement(string attributeName, string existingCode, string diagnosticId, string expectedCode, bool usePartialProperty)
253
298
{
299
var member = usePartialProperty
300
? $$"""
301
[ProfileProperty]
302
[{{attributeName}}("{{existingCode}}")]
303
public partial int Value { get; set; }
304
"""
305
: $$"""
306
[ProfileProperty]
307
[{{attributeName}}("{{existingCode}}")]
308
private int value;
309
""";
254
310
var source = $$"""
255
311
using XFEExtension.NetCore.AutoConfig;
256
312
public partial class Settings : XFEProfile
257
313
{
258
[ProfileProperty]
259
[{{attributeName}}("{{existingCode}}")]
260
private int value;
314
{{member}}
261
315
}
262
316
""";
263
317
using var workspace = new AdhocWorkspace();
@@ -11,7 +11,7 @@ public partial class XfeRoundTripProfile : XFEProfile
11
11
public partial class PartialPropertyProfile : XFEProfile
12
12
{
13
13
[ProfileProperty]
14
public partial string InstanceValue { get; set; } = "partial-default";
14
public partial string Value { get; set; } = "partial-default";
15
15
16
16
public PartialPropertyProfile() => DefaultProfileOperationMode = ProfileOperationMode.Xml;
17
17
}
@@ -33,8 +33,8 @@ public sealed class PersistenceTests
33
33
path => XmlRoundTripProfile.ProfilePath = path);
34
34
await AssertRoundTripAsync(
35
35
Path.Combine(directory, nameof(PartialPropertyProfile)),
36
value => PartialPropertyProfile.Value = value,
37
() => PartialPropertyProfile.Value,
36
value => PartialPropertyProfile.Current.Value = value,
37
() => PartialPropertyProfile.Current.Value,
38
38
() => PartialPropertyProfile.SaveProfileAsync(),
39
39
PartialPropertyProfile.LoadProfile,
40
40
path => PartialPropertyProfile.ProfilePath = path);
@@ -51,7 +51,7 @@ public sealed class PersistenceTests
51
51
XmlRoundTripProfile.Current.InstanceValue = "canonical";
52
52
53
53
var xml = XmlRoundTripProfile.ExportProfile();
54
PartialPropertyProfile.Current.InstanceValue = "partial-canonical";
54
PartialPropertyProfile.Current.Value = "partial-canonical";
55
55
var partialXml = PartialPropertyProfile.ExportProfile();
56
56
57
57
Assert.Contains("<Value>canonical</Value>", xml);
@@ -60,6 +60,28 @@ public sealed class PersistenceTests
60
60
Assert.DoesNotContain("<InstanceValue>", partialXml);
61
61
}
62
62
63
[Fact]
64
public async Task NaturalPartialPropertyRequestsAutomaticSave()
65
{
66
var directory = CreateTestDirectory();
67
try
68
{
69
PartialPropertyProfile.ProfilePath = Path.Combine(directory, nameof(PartialPropertyProfile));
70
PartialPropertyProfile.Current.AutoSaveDelay = TimeSpan.FromMilliseconds(10);
71
72
PartialPropertyProfile.Current.Value = "auto-saved";
73
await PartialPropertyProfile.FlushAsync();
74
75
var xml = await File.ReadAllTextAsync(PartialPropertyProfile.ProfilePath + ".xml");
76
Assert.Contains("<Value>auto-saved</Value>", xml);
77
}
78
finally
79
{
80
PartialPropertyProfile.DeleteProfile();
81
DeleteDirectory(directory);
82
}
83
}
84
63
85
[Fact]
64
86
public void ProfilePathAttributeAndDefaultPathUseExpectedPaths()
65
87
{
@@ -31,7 +31,7 @@
31
31
- 明确 Native AOT/Trim 支持边界并标注反射序列化入口
32
32
</PackageReleaseNotes>
33
33
<PackageLicenseFile>LICENSE.txt</PackageLicenseFile>
34
<Version>4.1.0</Version>
34
<Version>4.1.1</Version>
35
35
<GenerateDocumentationFile>True</GenerateDocumentationFile>
36
36
</PropertyGroup>
37
37