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

XFEExtension.NetCore.AutoConfig

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

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

XFEstudio/XFEExtension.NetCore.AutoConfig

优化 ProfileProperty 部分属性支持及生成器逻辑

支持自然名称声明部分属性,持久化名与属性一致。生成器不再生成同名静态门面属性,需通过 Current 实例访问。CodeFix 自动重写静态访问为 Current.Xxx。支持自定义持久化名称,完善注册、序列化、集合绑定和自动保存逻辑。同步更新测试用例和文档,版本号升至 4.1.1。

53a5aca
XFE工作室室长 <mail@xfegzs.com>
提交于

代码差异

9 个文件 +292 -100
Modified README.md +12 -11
@@ -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
Modified README.zh-CN.md +12 -11
@@ -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
Modified XFEExtension.NetCore.AutoConfig.Analyzer/Diagnostics/AutoConfigDiagnostics.cs +7 -7
@@ -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 {
Modified XFEExtension.NetCore.AutoConfig.Analyzer/Generator/ProfilePropertyAutoGenerator.cs +85 -24
@@ -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)
Modified XFEExtension.NetCore.AutoConfig.CodeFix/AutoConfigCodeFixProvider.cs +80 -27
@@ -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 {
Modified XFEExtension.NetCore.AutoConfig.Tests/GeneratorAndCodeFixTests.cs +69 -15
@@ -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();
Modified XFEExtension.NetCore.AutoConfig.Tests/PersistenceProfiles.cs +1 -1
@@ -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 }
Modified XFEExtension.NetCore.AutoConfig.Tests/PersistenceTests.cs +25 -3
@@ -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 {
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.0</Version>
34 <Version>4.1.1</Version>
35 35 <GenerateDocumentationFile>True</GenerateDocumentationFile>
36 36 </PropertyGroup>
37 37