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

XFEExtension.NetCore.AutoConfig

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

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

XFEstudio/XFEExtension.NetCore.AutoConfig

配置文件自动保存合并与线程安全支持

- ProfileList/Dictionary 增加内部锁,所有操作线程安全,集合变更合并自动保存 - XFEProfile 支持 AutoSaveDelay 合并窗口,后台保存异常暴露,采用原子写入 - 自动生成代码静态成员加锁,set 时自动请求保存 - 文档详细说明合并机制、并发安全、异常处理 - 新增并发保存合并等测试用例,验证正确性 - 版本号升级至 3.0.1

121cac6
XFE工作室室长 <mail@xfegzs.com>
提交于

代码差异

11 个文件 +929 -130
Modified README.md +21 -2
@@ -48,11 +48,28 @@ class Program
48 48 ```
49 49
50 50 > **Note:** The `[AutoLoadProfile]` attribute instructs the framework to call `LoadProfile()` inside the static constructor, so the configuration is restored automatically when the program starts.
51 >
52 > Automatic saves are coalesced over a 100 ms window by default. A burst of assignments therefore produces one complete file write instead of one write per property. Call `SaveProfile()` when the current snapshot must be flushed immediately.
51 53
52 54 ---
53 55
54 56 ## Detailed Usage
55 57
58 ### Automatic Save Performance and Concurrency
59
60 Generated properties, `ProfileList<T>`, and `ProfileDictionary<TKey, TValue>` are synchronized for concurrent access. Automatic save requests are handled by one writer per profile and coalesced using `AutoSaveDelay`. Files are committed through a temporary file and atomic replacement, so readers never observe a partially written configuration.
61
62 The coalescing window can be customized in the profile constructor:
63
64 ```csharp
65 public SystemProfile()
66 {
67 AutoSaveDelay = TimeSpan.FromMilliseconds(500);
68 }
69 ```
70
71 `Current.LastSaveException` exposes the most recent background save failure and is cleared after a successful save. Explicit `SaveProfile()` calls remain synchronous and surface write failures directly.
72
56 73 ### Changing the Storage Format
57 74
58 75 Set `DefaultProfileOperationMode` inside the instance constructor to switch the storage format. The file extension is updated automatically:
@@ -166,7 +183,7 @@ partial class SystemProfile : XFEProfile
166 183
167 184 ### Storing Collections with `ProfileList` and `ProfileDictionary`
168 185
169 `ProfileList<T>` and `ProfileDictionary<TKey, TValue>` automatically trigger a save whenever the collection is modified (add, remove, clear, etc.):
186 `ProfileList<T>` and `ProfileDictionary<TKey, TValue>` request a coalesced automatic save whenever the collection is modified (add, remove, clear, index assignment, etc.). Their public operations and enumeration snapshots are safe to use concurrently:
170 187
171 188 ```csharp
172 189 [AutoLoadProfile]
@@ -338,7 +355,7 @@ For every `partial` class that inherits `XFEProfile` and uses `[ProfileProperty]
338 355 | `ProfilePath` | `static string` | Storage path (without extension) |
339 356 | `ProfileExtension` | `static string` | File extension (auto-detected when empty) |
340 357 | `LoadProfile()` | `static void` | Loads config from file |
341 | `SaveProfile()` | `static void` | Saves config to file |
358 | `SaveProfile()` | `static void` | Immediately saves config to file and waits for completion |
342 359 | `DeleteProfile()` | `static void` | Deletes the config file |
343 360 | `ExportProfile()` | `static string` | Exports config as a string |
344 361 | `ImportProfile(string)` | `static void` | Imports config from a string |
@@ -355,6 +372,8 @@ For every `partial` class that inherits `XFEProfile` and uses `[ProfileProperty]
355 372 | `LoadOperation` | `ProfileLoadOperation` | Custom load delegate |
356 373 | `SaveOperation` | `ProfileSaveOperation` | Custom save delegate |
357 374 | `ProfilesDefaultPath` | `static string` | Default root directory for all profile files |
375 | `AutoSaveDelay` | `TimeSpan` | Coalescing window for automatic saves (100 ms by default) |
376 | `LastSaveException` | `Exception?` | Most recent background save failure |
358 377
359 378 ---
360 379
Modified README.zh-CN.md +21 -2
@@ -48,11 +48,28 @@ class Program
48 48 ```
49 49
50 50 > **说明:** `[AutoLoadProfile]` 特性会让框架在静态构造函数中自动调用 `LoadProfile()`,程序启动时无需手动加载。
51 >
52 > 自动保存默认会在 100ms 窗口内合并。同一批属性赋值只会完整写入一次文件,不再每次赋值都写入;需要立即落盘时可显式调用 `SaveProfile()`。
51 53
52 54 ---
53 55
54 56 ## 详细用法
55 57
58 ### 自动保存性能与并发访问
59
60 生成属性、`ProfileList<T>` 和 `ProfileDictionary<TKey, TValue>` 均支持并发访问。每个配置实例只有一个写入器,自动保存请求会按照 `AutoSaveDelay` 合并;写入时先生成同目录临时文件,再原子替换目标文件,避免其他线程读取到写了一半的配置。
61
62 可以在配置类构造函数中调整合并窗口:
63
64 ```csharp
65 public SystemProfile()
66 {
67 AutoSaveDelay = TimeSpan.FromMilliseconds(500);
68 }
69 ```
70
71 `Current.LastSaveException` 可用于检查最近一次后台保存异常,成功保存后会自动清空。显式调用 `SaveProfile()` 仍是同步立即保存,写入异常会直接抛给调用方。
72
56 73 ### 修改存储格式
57 74
58 75 通过在实例构造函数中设置 `DefaultProfileOperationMode` 来更改存储格式,文件扩展名会自动更改:
@@ -159,7 +176,7 @@ partial class SystemProfile : XFEProfile
159 176
160 177 ### 使用 `ProfileList` 和 `ProfileDictionary` 存储集合
161 178
162 `ProfileList<T>` 和 `ProfileDictionary<TKey, TValue>` 在集合发生变更(添加、删除等操作)时会自动触发保存:
179 `ProfileList<T>` 和 `ProfileDictionary<TKey, TValue>` 在集合发生变更(添加、删除、清空、索引赋值等操作)时会请求合并自动保存;其公开操作和快照枚举均可安全地并发使用:
163 180
164 181 ```csharp
165 182 [AutoLoadProfile]
@@ -331,7 +348,7 @@ SystemProfile.ImportProfile(exported); // 从字符串导入配置
331 348 | `ProfilePath` | `static string` | 配置文件路径(不含扩展名) |
332 349 | `ProfileExtension` | `static string` | 配置文件扩展名(空则自动推断) |
333 350 | `LoadProfile()` | `static void` | 从文件加载配置 |
334 | `SaveProfile()` | `static void` | 将配置保存到文件 |
351 | `SaveProfile()` | `static void` | 立即保存配置并等待写入完成 |
335 352 | `DeleteProfile()` | `static void` | 删除配置文件 |
336 353 | `ExportProfile()` | `static string` | 导出配置为字符串 |
337 354 | `ImportProfile(string)` | `static void` | 从字符串导入配置 |
@@ -348,6 +365,8 @@ SystemProfile.ImportProfile(exported); // 从字符串导入配置
348 365 | `LoadOperation` | `ProfileLoadOperation` | 自定义加载委托 |
349 366 | `SaveOperation` | `ProfileSaveOperation` | 自定义保存委托 |
350 367 | `ProfilesDefaultPath` | `static string` | 所有配置文件的默认根目录 |
368 | `AutoSaveDelay` | `TimeSpan` | 自动保存合并窗口(默认 100ms) |
369 | `LastSaveException` | `Exception?` | 最近一次后台保存异常 |
351 370
352 371 ---
353 372
Modified XFEExtension.NetCore.AutoConfig.Analyzer/Generator/ProfilePropertyAutoGenerator.cs +56 -20
@@ -148,10 +148,10 @@ public class ProfilePropertyAutoGenerator : IIncrementalGenerator
148 148 /// ○ <seealso langword=""{fieldName}""/> = <seealso langword=""value""/>;<br/>";
149 149 #endregion
150 150 }
151 setExpressionStatements.Add(SyntaxFactory.ExpressionStatement(SyntaxFactory.ParseExpression($"{className}.SaveProfile()")));
151 setExpressionStatements.Add(SyntaxFactory.ExpressionStatement(SyntaxFactory.ParseExpression("Current.InstanceRequestSaveProfile()")));
152 152 #region Set方法中的保存方法的注释
153 153 triviaText += $@"
154 /// ○ <seealso cref=""{className}.SaveProfile()""/>";
154 /// ○ 请求合并自动保存";
155 155 #endregion
156 156 #region Trivia尾
157 157 triviaText += @"
@@ -166,9 +166,21 @@ public class ProfilePropertyAutoGenerator : IIncrementalGenerator
166 166 SyntaxFactory.List(
167 167 [
168 168 SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration)
169 .WithBody(SyntaxFactory.Block(getExpressionStatements)),
169 .WithBody(SyntaxFactory.Block(
170 SyntaxFactory.LockStatement(
171 SyntaxFactory.ParseExpression("__profileInstanceSyncRoot"),
172 SyntaxFactory.Block(
173 SyntaxFactory.LockStatement(
174 SyntaxFactory.ParseExpression("Current.ProfileSyncRoot"),
175 SyntaxFactory.Block(getExpressionStatements)))))),
170 176 SyntaxFactory.AccessorDeclaration(SyntaxKind.SetAccessorDeclaration)
171 .WithBody(SyntaxFactory.Block(setExpressionStatements))
177 .WithBody(SyntaxFactory.Block(
178 SyntaxFactory.LockStatement(
179 SyntaxFactory.ParseExpression("__profileInstanceSyncRoot"),
180 SyntaxFactory.Block(
181 SyntaxFactory.LockStatement(
182 SyntaxFactory.ParseExpression("Current.ProfileSyncRoot"),
183 SyntaxFactory.Block(setExpressionStatements))))))
172 184 ])))
173 185 .WithLeadingTrivia(SyntaxFactory.ParseLeadingTrivia(triviaText));
174 186 var getMethod = SyntaxFactory.MethodDeclaration(SyntaxFactory.ParseTypeName("void"), getMethodName)
@@ -195,11 +207,17 @@ public class ProfilePropertyAutoGenerator : IIncrementalGenerator
195 207 SyntaxFactory.List(
196 208 [
197 209 SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration)
198 .WithExpressionBody(SyntaxFactory.ArrowExpressionClause(SyntaxFactory.ParseExpression($"{fieldName}")))
199 .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)),
210 .WithBody(SyntaxFactory.Block(
211 SyntaxFactory.LockStatement(
212 SyntaxFactory.ParseExpression("ProfileSyncRoot"),
213 SyntaxFactory.Block(
214 SyntaxFactory.ReturnStatement(SyntaxFactory.ParseExpression(fieldName)))))),
200 215 SyntaxFactory.AccessorDeclaration(SyntaxKind.SetAccessorDeclaration)
201 .WithExpressionBody(SyntaxFactory.ArrowExpressionClause(SyntaxFactory.ParseExpression($"{fieldName} = value")))
202 .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken))
216 .WithBody(SyntaxFactory.Block(
217 SyntaxFactory.LockStatement(
218 SyntaxFactory.ParseExpression("ProfileSyncRoot"),
219 SyntaxFactory.Block(
220 SyntaxFactory.ExpressionStatement(SyntaxFactory.ParseExpression($"{fieldName} = value"))))))
203 221 ])))
204 222 .WithLeadingTrivia(SyntaxFactory.ParseLeadingTrivia($@"/// <inheritdoc cref=""{fieldName}""/>
205 223 /// <remarks>
@@ -304,15 +322,23 @@ public class ProfilePropertyAutoGenerator : IIncrementalGenerator
304 322 {
305 323 memberDeclarationSyntaxes.Add(staticConstructorSyntax);
306 324 }
325 memberDeclarationSyntaxes.Add(SyntaxFactory.ParseMemberDeclaration("private static readonly object __profileInstanceSyncRoot = new();"));
326 memberDeclarationSyntaxes.Add(SyntaxFactory.ParseMemberDeclaration($"private static {className} __current = null;"));
307 327 memberDeclarationSyntaxes.Add(SyntaxFactory.PropertyDeclaration(SyntaxFactory.ParseTypeName(className), "Current")
308 328 .WithModifiers(SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.PublicKeyword), SyntaxFactory.Token(SyntaxKind.StaticKeyword)))
309 329 .AddAttributeLists(SyntaxFactory.AttributeList(SyntaxFactory.SingletonSeparatedList(SyntaxFactory.Attribute(SyntaxFactory.ParseName("global::XFEExtension.NetCore.AutoConfig.ProfileInstanceAttribute")))))
310 330 .WithAccessorList(SyntaxFactory.AccessorList(SyntaxFactory.List(
311 331 [
312 332 SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration)
313 .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)),
333 .WithBody(SyntaxFactory.Block(
334 SyntaxFactory.LockStatement(
335 SyntaxFactory.ParseExpression("__profileInstanceSyncRoot"),
336 SyntaxFactory.Block(SyntaxFactory.ParseStatement("return __current;"))))),
314 337 SyntaxFactory.AccessorDeclaration(SyntaxKind.SetAccessorDeclaration)
315 .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken))
338 .WithBody(SyntaxFactory.Block(
339 SyntaxFactory.LockStatement(
340 SyntaxFactory.ParseExpression("__profileInstanceSyncRoot"),
341 SyntaxFactory.Block(SyntaxFactory.ParseStatement("__current = value;")))))
316 342 ])))
317 343 .WithLeadingTrivia(SyntaxFactory.ParseLeadingTrivia($@"/// <summary>
318 344 /// 该配置文件的实例<br/><br/>
@@ -353,8 +379,10 @@ public class ProfilePropertyAutoGenerator : IIncrementalGenerator
353 379 ")));
354 380 memberDeclarationSyntaxes.Add(SyntaxFactory.MethodDeclaration(SyntaxFactory.ParseTypeName("void"), "LoadProfile")
355 381 .WithModifiers(SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.PublicKeyword), SyntaxFactory.Token(SyntaxKind.StaticKeyword)))
356 .WithExpressionBody(SyntaxFactory.ArrowExpressionClause(SyntaxFactory.ParseExpression($"Current = Current.InstanceLoadProfile() as {className}")))
357 .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken))
382 .WithBody(SyntaxFactory.Block(
383 SyntaxFactory.LockStatement(
384 SyntaxFactory.ParseExpression("__profileInstanceSyncRoot"),
385 SyntaxFactory.Block(SyntaxFactory.ParseStatement($"Current = Current.InstanceLoadProfile() as {className};")))))
358 386 .WithLeadingTrivia(SyntaxFactory.ParseLeadingTrivia($@"/// <summary>
359 387 /// 配置文件加载方法<br/><br/>
360 388 /// <seealso cref=""{className}.LoadProfile""/> 是根据 <seealso cref=""{className}""/> 生成的加载配置文件的静态方法
@@ -362,8 +390,10 @@ public class ProfilePropertyAutoGenerator : IIncrementalGenerator
362 390 ")));
363 391 memberDeclarationSyntaxes.Add(SyntaxFactory.MethodDeclaration(SyntaxFactory.ParseTypeName("void"), "SaveProfile")
364 392 .WithModifiers(SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.PublicKeyword), SyntaxFactory.Token(SyntaxKind.StaticKeyword)))
365 .WithExpressionBody(SyntaxFactory.ArrowExpressionClause(SyntaxFactory.ParseExpression($"Current.InstanceSaveProfile()")))
366 .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken))
393 .WithBody(SyntaxFactory.Block(
394 SyntaxFactory.LockStatement(
395 SyntaxFactory.ParseExpression("__profileInstanceSyncRoot"),
396 SyntaxFactory.Block(SyntaxFactory.ParseStatement("Current.InstanceSaveProfile();")))))
367 397 .WithLeadingTrivia(SyntaxFactory.ParseLeadingTrivia($@"/// <summary>
368 398 /// 配置文件保存方法<br/><br/>
369 399 /// <seealso cref=""{className}.SaveProfile""/> 是根据 <seealso cref=""{className}""/> 生成的保存配置文件的静态方法
@@ -371,8 +401,10 @@ public class ProfilePropertyAutoGenerator : IIncrementalGenerator
371 401 ")));
372 402 memberDeclarationSyntaxes.Add(SyntaxFactory.MethodDeclaration(SyntaxFactory.ParseTypeName("void"), "DeleteProfile")
373 403 .WithModifiers(SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.PublicKeyword), SyntaxFactory.Token(SyntaxKind.StaticKeyword)))
374 .WithExpressionBody(SyntaxFactory.ArrowExpressionClause(SyntaxFactory.ParseExpression($"Current.InstanceDeleteProfile()")))
375 .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken))
404 .WithBody(SyntaxFactory.Block(
405 SyntaxFactory.LockStatement(
406 SyntaxFactory.ParseExpression("__profileInstanceSyncRoot"),
407 SyntaxFactory.Block(SyntaxFactory.ParseStatement("Current.InstanceDeleteProfile();")))))
376 408 .WithLeadingTrivia(SyntaxFactory.ParseLeadingTrivia($@"/// <summary>
377 409 /// 配置文件删除方法<br/><br/>
378 410 /// <seealso cref=""{className}.DeleteProfile""/> 是根据 <seealso cref=""{className}""/> 生成的删除配置文件的静态方法
@@ -380,8 +412,10 @@ public class ProfilePropertyAutoGenerator : IIncrementalGenerator
380 412 ")));
381 413 memberDeclarationSyntaxes.Add(SyntaxFactory.MethodDeclaration(SyntaxFactory.ParseTypeName("string"), "ExportProfile")
382 414 .WithModifiers(SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.PublicKeyword), SyntaxFactory.Token(SyntaxKind.StaticKeyword)))
383 .WithExpressionBody(SyntaxFactory.ArrowExpressionClause(SyntaxFactory.ParseExpression($"Current.InstanceExportProfile()")))
384 .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken))
415 .WithBody(SyntaxFactory.Block(
416 SyntaxFactory.LockStatement(
417 SyntaxFactory.ParseExpression("__profileInstanceSyncRoot"),
418 SyntaxFactory.Block(SyntaxFactory.ParseStatement("return Current.InstanceExportProfile();")))))
385 419 .WithLeadingTrivia(SyntaxFactory.ParseLeadingTrivia($@"/// <summary>
386 420 /// 配置文件导出方法<br/><br/>
387 421 /// <seealso cref=""{className}.ExportProfile""/> 是根据 <seealso cref=""{className}""/> 生成的导出配置文件的静态方法
@@ -391,8 +425,10 @@ public class ProfilePropertyAutoGenerator : IIncrementalGenerator
391 425 memberDeclarationSyntaxes.Add(SyntaxFactory.MethodDeclaration(SyntaxFactory.ParseTypeName("void"), "ImportProfile")
392 426 .WithModifiers(SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.PublicKeyword), SyntaxFactory.Token(SyntaxKind.StaticKeyword)))
393 427 .AddParameterListParameters(SyntaxFactory.Parameter(SyntaxFactory.Identifier("profileString")).WithType(SyntaxFactory.ParseTypeName("string")))
394 .WithExpressionBody(SyntaxFactory.ArrowExpressionClause(SyntaxFactory.ParseExpression($"Current = Current.InstanceImportProfile(profileString) as {className}")))
395 .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken))
428 .WithBody(SyntaxFactory.Block(
429 SyntaxFactory.LockStatement(
430 SyntaxFactory.ParseExpression("__profileInstanceSyncRoot"),
431 SyntaxFactory.Block(SyntaxFactory.ParseStatement($"Current = Current.InstanceImportProfile(profileString) as {className};")))))
396 432 .WithLeadingTrivia(SyntaxFactory.ParseLeadingTrivia($@"/// <summary>
397 433 /// 配置文件导入方法<br/><br/>
398 434 /// <seealso cref=""{className}.ImportProfile""/> 是根据 <seealso cref=""{className}""/> 生成的导入配置文件的静态方法
Added XFEExtension.NetCore.AutoConfig.Tests/ConcurrentProfile.cs +46 -0
@@ -0,0 +1,46 @@
1 using System.Globalization;
2
3 namespace XFEExtension.NetCore.AutoConfig.Tests;
4
5 public partial class ConcurrentProfile : XFEProfile
6 {
7 private static int saveCount;
8
9 [ProfileProperty]
10 private int value;
11
12 [ProfileProperty]
13 [ProfilePropertyAddGet("Current.numbers.CurrentProfile = Current")]
14 [ProfilePropertyAddGet("return Current.numbers")]
15 private ProfileList<int> numbers = [];
16
17 [ProfileProperty]
18 [ProfilePropertyAddGet("Current.lookup.CurrentProfile = Current")]
19 [ProfilePropertyAddGet("return Current.lookup")]
20 private ProfileDictionary<int, int> lookup = [];
21
22 public ConcurrentProfile()
23 {
24 DefaultProfileOperationMode = ProfileOperationMode.Custom;
25 LoadOperation = static (_, _, _, _) => null;
26 SaveOperation = SaveSnapshot;
27 }
28
29 public static int SaveCount => Volatile.Read(ref saveCount);
30
31 public static void ResetSaveCount() => Interlocked.Exchange(ref saveCount, 0);
32
33 private static string SaveSnapshot(XFEProfile profileInstance, Dictionary<string, Type> propertyInfoDictionary, Dictionary<string, GetValueDelegate> propertyGetFuncDictionary)
34 {
35 Interlocked.Increment(ref saveCount);
36 var currentValue = (int)(propertyGetFuncDictionary[nameof(Value)]() ?? 0);
37 var currentNumbers = (ProfileList<int>)propertyGetFuncDictionary[nameof(Numbers)]()!;
38 var currentLookup = (ProfileDictionary<int, int>)propertyGetFuncDictionary[nameof(Lookup)]()!;
39 return string.Join('|', currentValue.ToString(CultureInfo.InvariantCulture), currentNumbers.Count.ToString(CultureInfo.InvariantCulture), currentLookup.Count.ToString(CultureInfo.InvariantCulture));
40 }
41 }
42
43 public sealed class SerializationProfile : XFEProfile
44 {
45 public string Name { get; set; } = string.Empty;
46 }
Added XFEExtension.NetCore.AutoConfig.Tests/ConcurrentSaveTests.cs +88 -0
@@ -0,0 +1,88 @@
1 using Xunit;
2
3 namespace XFEExtension.NetCore.AutoConfig.Tests;
4
5 public class ConcurrentSaveTests
6 {
7 [Fact]
8 public void RuntimeSaveStateIsNotIncludedInJsonOrXmlProfiles()
9 {
10 var profile = new SerializationProfile { Name = "Test" };
11
12 var json = XFEProfile.JsonSaveProfileOperation(profile, [], []);
13 var xml = XFEProfile.XmlSaveProfileOperation(profile, [], []);
14
15 Assert.Contains("Name", json);
16 Assert.Contains("Name", xml);
17 Assert.DoesNotContain(nameof(XFEProfile.AutoSaveDelay), json);
18 Assert.DoesNotContain(nameof(XFEProfile.LastSaveException), json);
19 Assert.DoesNotContain(nameof(XFEProfile.AutoSaveDelay), xml);
20 Assert.DoesNotContain(nameof(XFEProfile.LastSaveException), xml);
21 }
22
23 [Fact]
24 public async Task ConcurrentChangesAreCoalescedAndPersistTheLatestSnapshot()
25 {
26 var profileDirectory = Path.Combine(Path.GetTempPath(), "XFEAutoConfigTests", Guid.NewGuid().ToString("N"));
27 Directory.CreateDirectory(profileDirectory);
28 XFEProfile.ProfilesDefaultPath = profileDirectory;
29 var profilePath = Path.Combine(profileDirectory, $"{nameof(ConcurrentProfile)}.xpf");
30
31 try
32 {
33 ConcurrentProfile.Current.AutoSaveDelay = TimeSpan.FromMilliseconds(250);
34 ConcurrentProfile.ResetSaveCount();
35 var numbers = ConcurrentProfile.Numbers;
36 var lookup = ConcurrentProfile.Lookup;
37
38 Parallel.Invoke(
39 () => Parallel.For(0, 2_000, index => ConcurrentProfile.Value = index),
40 () => Parallel.For(0, 1_000, numbers.Add),
41 () => Parallel.For(0, 1_000, index => lookup.TryAdd(index, index)));
42
43 const int finalValue = 123_456;
44 ConcurrentProfile.Value = finalValue;
45 var expectedContent = $"{finalValue}|1000|1000";
46 await WaitForFileContentAsync(profilePath, expectedContent, TimeSpan.FromSeconds(10));
47 await Task.Delay(500);
48
49 Assert.Equal(1_000, numbers.Count);
50 Assert.Equal(1_000, lookup.Count);
51 Assert.Null(ConcurrentProfile.Current.LastSaveException);
52 Assert.InRange(ConcurrentProfile.SaveCount, 1, 99);
53
54 var saveCountBeforeManualSave = ConcurrentProfile.SaveCount;
55 ConcurrentProfile.SaveProfile();
56 Assert.Equal(saveCountBeforeManualSave + 1, ConcurrentProfile.SaveCount);
57 Assert.Equal(expectedContent, await File.ReadAllTextAsync(profilePath));
58 Assert.Empty(Directory.EnumerateFiles(profileDirectory, "*.tmp"));
59 }
60 finally
61 {
62 ConcurrentProfile.DeleteProfile();
63 if (Directory.Exists(profileDirectory))
64 Directory.Delete(profileDirectory, true);
65 }
66 }
67
68 private static async Task WaitForFileContentAsync(string profilePath, string expectedContent, TimeSpan timeout)
69 {
70 var timeoutAt = DateTime.UtcNow + timeout;
71 while (DateTime.UtcNow < timeoutAt)
72 {
73 if (File.Exists(profilePath))
74 {
75 try
76 {
77 if (await File.ReadAllTextAsync(profilePath) == expectedContent)
78 return;
79 }
80 catch (IOException)
81 {
82 }
83 }
84 await Task.Delay(25);
85 }
86 throw new TimeoutException($"配置文件未在规定时间内写入预期内容:{profilePath}");
87 }
88 }
Added XFEExtension.NetCore.AutoConfig.Tests/XFEExtension.NetCore.AutoConfig.Tests.csproj +25 -0
@@ -0,0 +1,25 @@
1 <Project Sdk="Microsoft.NET.Sdk">
2
3 <PropertyGroup>
4 <TargetFramework>net10.0</TargetFramework>
5 <ImplicitUsings>enable</ImplicitUsings>
6 <Nullable>enable</Nullable>
7 <IsPackable>false</IsPackable>
8 <IsTestProject>true</IsTestProject>
9 </PropertyGroup>
10
11 <ItemGroup>
12 <PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.8.1" />
13 <PackageReference Include="xunit" Version="2.9.3" />
14 <PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
15 <PrivateAssets>all</PrivateAssets>
16 <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
17 </PackageReference>
18 </ItemGroup>
19
20 <ItemGroup>
21 <ProjectReference Include="..\XFEExtension.NetCore.AutoConfig\XFEExtension.NetCore.AutoConfig.csproj" />
22 <ProjectReference Include="..\XFEExtension.NetCore.AutoConfig.Analyzer\XFEExtension.NetCore.AutoConfig.Analyzer.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
23 </ItemGroup>
24
25 </Project>
Modified XFEExtension.NetCore.AutoConfig.slnx +1 -0
@@ -1,6 +1,7 @@
1 1 <Solution>
2 2 <Project Path="AutoConfig.Analyzer.Test/AutoConfig.Analyzer.Test.csproj" />
3 3 <Project Path="XFEExtension.NetCore.AutoConfig.Analyzer/XFEExtension.NetCore.AutoConfig.Analyzer.csproj" />
4 <Project Path="XFEExtension.NetCore.AutoConfig.Tests/XFEExtension.NetCore.AutoConfig.Tests.csproj" />
4 5 <Project Path="XFEExtension.NetCore.AutoConfig/XFEExtension.NetCore.AutoConfig.csproj">
5 6 <BuildDependency Project="XFEExtension.NetCore.AutoConfig.Analyzer/XFEExtension.NetCore.AutoConfig.Analyzer.csproj" />
6 7 </Project>
Modified XFEExtension.NetCore.AutoConfig/ProfileDictionary.cs +220 -45
@@ -1,17 +1,19 @@
1 using System.Collections;
1 using System.Collections;
2 2 using System.Diagnostics.CodeAnalysis;
3 3 using System.Runtime.Serialization;
4 4
5 5 namespace XFEExtension.NetCore.AutoConfig;
6 6
7 7 /// <summary>
8 /// 配置文件字典
8 /// 支持线程安全访问和合并自动保存的配置文件字典
9 9 /// </summary>
10 10 /// <typeparam name="TKey">字典Key泛型</typeparam>
11 11 /// <typeparam name="TValue">字典Value泛型</typeparam>
12 12 public class ProfileDictionary<TKey, TValue> : ICollection<KeyValuePair<TKey, TValue>>, IEnumerable<KeyValuePair<TKey, TValue>>, IEnumerable, IDictionary<TKey, TValue>, IReadOnlyCollection<KeyValuePair<TKey, TValue>>, IReadOnlyDictionary<TKey, TValue>, ICollection, IDictionary, IDeserializationCallback, ISerializable where TKey : notnull
13 13 {
14 14 private readonly Dictionary<TKey, TValue> _innerDictionary;
15 private readonly object _syncRoot = new();
16 private XFEProfile? _currentProfile;
15 17
16 18 /// <summary>
17 19 /// 创建空字典
@@ -25,135 +27,308 @@ public class ProfileDictionary<TKey, TValue> : ICollection<KeyValuePair<TKey, TV
25 27 public ProfileDictionary(Dictionary<TKey, TValue> innerDictionary) => _innerDictionary = innerDictionary;
26 28
27 29 ///<inheritdoc/>
28 public TValue this[TKey key] { get => ((IDictionary<TKey, TValue>)_innerDictionary)[key]; set => ((IDictionary<TKey, TValue>)_innerDictionary)[key] = value; }
30 public TValue this[TKey key]
31 {
32 get
33 {
34 lock (_syncRoot)
35 return _innerDictionary[key];
36 }
37 set
38 {
39 lock (_syncRoot)
40 {
41 _innerDictionary[key] = value;
42 RequestSave();
43 }
44 }
45 }
46
29 47 ///<inheritdoc/>
30 public object? this[object key] { get => ((IDictionary)_innerDictionary)[key]; set => ((IDictionary)_innerDictionary)[key] = value; }
48 public object? this[object key]
49 {
50 get
51 {
52 lock (_syncRoot)
53 return ((IDictionary)_innerDictionary)[key];
54 }
55 set
56 {
57 lock (_syncRoot)
58 {
59 ((IDictionary)_innerDictionary)[key] = value;
60 RequestSave();
61 }
62 }
63 }
31 64
32 65 ///<inheritdoc/>
33 public int Count => ((ICollection<KeyValuePair<TKey, TValue>>)_innerDictionary).Count;
66 public int Count
67 {
68 get
69 {
70 lock (_syncRoot)
71 return _innerDictionary.Count;
72 }
73 }
34 74
35 75 /// <summary>
36 76 /// 当前配置文件实例
37 77 /// </summary>
38 public XFEProfile? CurrentProfile { get; set; }
78 public XFEProfile? CurrentProfile
79 {
80 get
81 {
82 lock (_syncRoot)
83 return _currentProfile;
84 }
85 set
86 {
87 lock (_syncRoot)
88 _currentProfile = value;
89 }
90 }
39 91
40 92 ///<inheritdoc/>
41 93 public bool IsReadOnly => ((ICollection<KeyValuePair<TKey, TValue>>)_innerDictionary).IsReadOnly;
42 94
43 95 ///<inheritdoc/>
44 public ICollection<TKey> Keys => ((IDictionary<TKey, TValue>)_innerDictionary).Keys;
96 public ICollection<TKey> Keys
97 {
98 get
99 {
100 lock (_syncRoot)
101 return _innerDictionary.Keys.ToArray();
102 }
103 }
45 104
46 105 ///<inheritdoc/>
47 public ICollection<TValue> Values => ((IDictionary<TKey, TValue>)_innerDictionary).Values;
106 public ICollection<TValue> Values
107 {
108 get
109 {
110 lock (_syncRoot)
111 return _innerDictionary.Values.ToArray();
112 }
113 }
48 114
49 115 ///<inheritdoc/>
50 public bool IsSynchronized => ((ICollection)_innerDictionary).IsSynchronized;
116 public bool IsSynchronized => true;
51 117
52 118 ///<inheritdoc/>
53 public object SyncRoot => ((ICollection)_innerDictionary).SyncRoot;
119 public object SyncRoot => _syncRoot;
54 120
55 121 ///<inheritdoc/>
56 122 public bool IsFixedSize => ((IDictionary)_innerDictionary).IsFixedSize;
57 123
58 IEnumerable<TKey> IReadOnlyDictionary<TKey, TValue>.Keys => ((IReadOnlyDictionary<TKey, TValue>)_innerDictionary).Keys;
124 IEnumerable<TKey> IReadOnlyDictionary<TKey, TValue>.Keys
125 {
126 get
127 {
128 lock (_syncRoot)
129 return _innerDictionary.Keys.ToArray();
130 }
131 }
59 132
60 ICollection IDictionary.Keys => ((IDictionary)_innerDictionary).Keys;
133 ICollection IDictionary.Keys
134 {
135 get
136 {
137 lock (_syncRoot)
138 return _innerDictionary.Keys.ToArray();
139 }
140 }
61 141
62 IEnumerable<TValue> IReadOnlyDictionary<TKey, TValue>.Values => ((IReadOnlyDictionary<TKey, TValue>)_innerDictionary).Values;
142 IEnumerable<TValue> IReadOnlyDictionary<TKey, TValue>.Values
143 {
144 get
145 {
146 lock (_syncRoot)
147 return _innerDictionary.Values.ToArray();
148 }
149 }
63 150
64 ICollection IDictionary.Values => ((IDictionary)_innerDictionary).Values;
151 ICollection IDictionary.Values
152 {
153 get
154 {
155 lock (_syncRoot)
156 return _innerDictionary.Values.ToArray();
157 }
158 }
65 159
66 160 ///<inheritdoc/>
67 161 public void Add(KeyValuePair<TKey, TValue> item)
68 162 {
69 ((ICollection<KeyValuePair<TKey, TValue>>)_innerDictionary).Add(item);
70 CurrentProfile?.InstanceSaveProfile();
163 lock (_syncRoot)
164 {
165 ((ICollection<KeyValuePair<TKey, TValue>>)_innerDictionary).Add(item);
166 RequestSave();
167 }
71 168 }
72 169
73 170 ///<inheritdoc/>
74 171 public void Add(TKey key, TValue value)
75 172 {
76 ((IDictionary<TKey, TValue>)_innerDictionary).Add(key, value);
77 CurrentProfile?.InstanceSaveProfile();
173 lock (_syncRoot)
174 {
175 _innerDictionary.Add(key, value);
176 RequestSave();
177 }
78 178 }
79 179
80 180 ///<inheritdoc/>
81 181 public void Add(object key, object? value)
82 182 {
83 ((IDictionary)_innerDictionary).Add(key, value);
84 CurrentProfile?.InstanceSaveProfile();
183 lock (_syncRoot)
184 {
185 ((IDictionary)_innerDictionary).Add(key, value);
186 RequestSave();
187 }
85 188 }
86 189
87 190 ///<inheritdoc/>
88 191 public void Clear()
89 192 {
90 ((ICollection<KeyValuePair<TKey, TValue>>)_innerDictionary).Clear();
91 CurrentProfile?.InstanceSaveProfile();
193 lock (_syncRoot)
194 {
195 if (_innerDictionary.Count == 0)
196 return;
197 _innerDictionary.Clear();
198 RequestSave();
199 }
92 200 }
93 201
94 202 ///<inheritdoc/>
95 public bool Contains(KeyValuePair<TKey, TValue> item) => ((ICollection<KeyValuePair<TKey, TValue>>)_innerDictionary).Contains(item);
203 public bool Contains(KeyValuePair<TKey, TValue> item)
204 {
205 lock (_syncRoot)
206 return ((ICollection<KeyValuePair<TKey, TValue>>)_innerDictionary).Contains(item);
207 }
96 208
97 209 ///<inheritdoc/>
98 public bool Contains(object key) => ((IDictionary)_innerDictionary).Contains(key);
210 public bool Contains(object key)
211 {
212 lock (_syncRoot)
213 return ((IDictionary)_innerDictionary).Contains(key);
214 }
99 215
100 216 ///<inheritdoc/>
101 public bool ContainsKey(TKey key) => ((IDictionary<TKey, TValue>)_innerDictionary).ContainsKey(key);
217 public bool ContainsKey(TKey key)
218 {
219 lock (_syncRoot)
220 return _innerDictionary.ContainsKey(key);
221 }
102 222
103 223 ///<inheritdoc/>
104 public bool ContainsValue(TValue value) => _innerDictionary.ContainsValue(value);
224 public bool ContainsValue(TValue value)
225 {
226 lock (_syncRoot)
227 return _innerDictionary.ContainsValue(value);
228 }
105 229
106 230 ///<inheritdoc/>
107 public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex) => ((ICollection<KeyValuePair<TKey, TValue>>)_innerDictionary).CopyTo(array, arrayIndex);
231 public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex)
232 {
233 lock (_syncRoot)
234 ((ICollection<KeyValuePair<TKey, TValue>>)_innerDictionary).CopyTo(array, arrayIndex);
235 }
108 236
109 237 ///<inheritdoc/>
110 public void CopyTo(Array array, int index) => ((ICollection)_innerDictionary).CopyTo(array, index);
238 public void CopyTo(Array array, int index)
239 {
240 lock (_syncRoot)
241 ((ICollection)_innerDictionary).CopyTo(array, index);
242 }
111 243
112 244 ///<inheritdoc/>
113 public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator() => ((IEnumerable<KeyValuePair<TKey, TValue>>)_innerDictionary).GetEnumerator();
245 public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
246 {
247 lock (_syncRoot)
248 return new Dictionary<TKey, TValue>(_innerDictionary, _innerDictionary.Comparer).GetEnumerator();
249 }
114 250
115 251 ///<inheritdoc/>
116 252 [Obsolete]
117 public void GetObjectData(SerializationInfo info, StreamingContext context) => ((ISerializable)_innerDictionary).GetObjectData(info, context);
253 public void GetObjectData(SerializationInfo info, StreamingContext context)
254 {
255 lock (_syncRoot)
256 ((ISerializable)_innerDictionary).GetObjectData(info, context);
257 }
118 258
119 259 ///<inheritdoc/>
120 public void OnDeserialization(object? sender) => ((IDeserializationCallback)_innerDictionary).OnDeserialization(sender);
260 public void OnDeserialization(object? sender)
261 {
262 lock (_syncRoot)
263 ((IDeserializationCallback)_innerDictionary).OnDeserialization(sender);
264 }
121 265
122 266 ///<inheritdoc/>
123 267 public bool Remove(KeyValuePair<TKey, TValue> item)
124 268 {
125 var result = ((ICollection<KeyValuePair<TKey, TValue>>)_innerDictionary).Remove(item);
126 CurrentProfile?.InstanceSaveProfile();
127 return result;
269 lock (_syncRoot)
270 {
271 var result = ((ICollection<KeyValuePair<TKey, TValue>>)_innerDictionary).Remove(item);
272 if (result)
273 RequestSave();
274 return result;
275 }
128 276 }
129 277
130 278 ///<inheritdoc/>
131 279 public bool Remove(TKey key)
132 280 {
133 var result = ((IDictionary<TKey, TValue>)_innerDictionary).Remove(key);
134 CurrentProfile?.InstanceSaveProfile();
135 return result;
281 lock (_syncRoot)
282 {
283 var result = _innerDictionary.Remove(key);
284 if (result)
285 RequestSave();
286 return result;
287 }
136 288 }
137 289
138 290 ///<inheritdoc/>
139 291 public void Remove(object key)
140 292 {
141 ((IDictionary)_innerDictionary).Remove(key);
142 CurrentProfile?.InstanceSaveProfile();
293 lock (_syncRoot)
294 {
295 var count = _innerDictionary.Count;
296 ((IDictionary)_innerDictionary).Remove(key);
297 if (_innerDictionary.Count != count)
298 RequestSave();
299 }
143 300 }
144 301
145 302 ///<inheritdoc/>
146 public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) => ((IDictionary<TKey, TValue>)_innerDictionary).TryGetValue(key, out value);
303 public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value)
304 {
305 lock (_syncRoot)
306 return _innerDictionary.TryGetValue(key, out value);
307 }
147 308
148 309 ///<inheritdoc/>
149 310 public bool TryAdd(TKey key, TValue value)
150 311 {
151 var result = _innerDictionary.TryAdd(key, value);
152 CurrentProfile?.InstanceSaveProfile();
153 return result;
312 lock (_syncRoot)
313 {
314 var result = _innerDictionary.TryAdd(key, value);
315 if (result)
316 RequestSave();
317 return result;
318 }
154 319 }
155 320
156 IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_innerDictionary).GetEnumerator();
321 IEnumerator IEnumerable.GetEnumerator()
322 {
323 lock (_syncRoot)
324 return ((IEnumerable)new Dictionary<TKey, TValue>(_innerDictionary, _innerDictionary.Comparer)).GetEnumerator();
325 }
326
327 IDictionaryEnumerator IDictionary.GetEnumerator()
328 {
329 lock (_syncRoot)
330 return ((IDictionary)new Dictionary<TKey, TValue>(_innerDictionary, _innerDictionary.Comparer)).GetEnumerator();
331 }
157 332
158 IDictionaryEnumerator IDictionary.GetEnumerator() => ((IDictionary)_innerDictionary).GetEnumerator();
333 private void RequestSave() => _currentProfile?.InstanceRequestSaveProfile();
159 334 }
Modified XFEExtension.NetCore.AutoConfig/ProfileList.cs +164 -38
@@ -1,14 +1,16 @@
1 using System.Collections;
1 using System.Collections;
2 2
3 3 namespace XFEExtension.NetCore.AutoConfig;
4 4
5 5 /// <summary>
6 /// 配置文件列表
6 /// 支持线程安全访问和合并自动保存的配置文件列表
7 7 /// </summary>
8 8 /// <typeparam name="TValue">列表泛型</typeparam>
9 9 public class ProfileList<TValue> : ICollection<TValue>, IEnumerable<TValue>, IEnumerable, IList<TValue>, IReadOnlyCollection<TValue>, IReadOnlyList<TValue>, ICollection, IList
10 10 {
11 11 private readonly List<TValue> _innerList;
12 private readonly object _syncRoot = new();
13 private XFEProfile? _currentProfile;
12 14
13 15 /// <summary>
14 16 /// 创建空列表
@@ -22,25 +24,75 @@ public class ProfileList<TValue> : ICollection<TValue>, IEnumerable<TValue>, IEn
22 24 public ProfileList(List<TValue> innerList) => _innerList = innerList;
23 25
24 26 ///<inheritdoc/>
25 public TValue this[int index] { get => ((IList<TValue>)_innerList)[index]; set => ((IList<TValue>)_innerList)[index] = value; }
26 object? IList.this[int index] { get => ((IList)_innerList)[index]; set => ((IList)_innerList)[index] = value; }
27 public TValue this[int index]
28 {
29 get
30 {
31 lock (_syncRoot)
32 return _innerList[index];
33 }
34 set
35 {
36 lock (_syncRoot)
37 {
38 _innerList[index] = value;
39 RequestSave();
40 }
41 }
42 }
43
44 object? IList.this[int index]
45 {
46 get
47 {
48 lock (_syncRoot)
49 return ((IList)_innerList)[index];
50 }
51 set
52 {
53 lock (_syncRoot)
54 {
55 ((IList)_innerList)[index] = value;
56 RequestSave();
57 }
58 }
59 }
27 60
28 61 /// <summary>
29 62 /// 当前配置文件实例
30 63 /// </summary>
31 public XFEProfile? CurrentProfile { get; set; }
64 public XFEProfile? CurrentProfile
65 {
66 get
67 {
68 lock (_syncRoot)
69 return _currentProfile;
70 }
71 set
72 {
73 lock (_syncRoot)
74 _currentProfile = value;
75 }
76 }
32 77
33 78 ///<inheritdoc/>
34 public int Count => ((ICollection<TValue>)_innerList).Count;
79 public int Count
80 {
81 get
82 {
83 lock (_syncRoot)
84 return _innerList.Count;
85 }
86 }
35 87
36 88 ///<inheritdoc/>
37 89 public bool IsReadOnly => ((ICollection<TValue>)_innerList).IsReadOnly;
38 90
39 91 ///<inheritdoc/>
40 public bool IsSynchronized => ((ICollection)_innerList).IsSynchronized;
92 public bool IsSynchronized => true;
41 93
42 94 ///<inheritdoc/>
43 public object SyncRoot => ((ICollection)_innerList).SyncRoot;
95 public object SyncRoot => _syncRoot;
44 96
45 97 ///<inheritdoc/>
46 98 public bool IsFixedSize => ((IList)_innerList).IsFixedSize;
@@ -48,95 +100,169 @@ public class ProfileList<TValue> : ICollection<TValue>, IEnumerable<TValue>, IEn
48 100 ///<inheritdoc/>
49 101 public void Add(TValue item)
50 102 {
51 ((ICollection<TValue>)_innerList).Add(item);
52 CurrentProfile?.InstanceSaveProfile();
103 lock (_syncRoot)
104 {
105 _innerList.Add(item);
106 RequestSave();
107 }
53 108 }
54 109
55 110 ///<inheritdoc/>
56 111 public int Add(object? value)
57 112 {
58 var result = ((IList)_innerList).Add(value);
59 CurrentProfile?.InstanceSaveProfile();
60 return result;
113 lock (_syncRoot)
114 {
115 var result = ((IList)_innerList).Add(value);
116 RequestSave();
117 return result;
118 }
61 119 }
62 120
63 121 ///<inheritdoc/>
64 122 public void AddRange(IEnumerable<TValue> collection)
65 123 {
66 _innerList.AddRange(collection);
67 CurrentProfile?.InstanceSaveProfile();
124 var items = collection.ToArray();
125 if (items.Length == 0)
126 return;
127 lock (_syncRoot)
128 {
129 _innerList.AddRange(items);
130 RequestSave();
131 }
68 132 }
69 133
70 134 ///<inheritdoc/>
71 135 public void AddRange(ReadOnlySpan<TValue> source)
72 136 {
73 _innerList.AddRange(source);
74 CurrentProfile?.InstanceSaveProfile();
137 if (source.IsEmpty)
138 return;
139 lock (_syncRoot)
140 {
141 _innerList.AddRange(source);
142 RequestSave();
143 }
75 144 }
76 145
77 146 ///<inheritdoc/>
78 147 public void Clear()
79 148 {
80 ((ICollection<TValue>)_innerList).Clear();
81 CurrentProfile?.InstanceSaveProfile();
149 lock (_syncRoot)
150 {
151 if (_innerList.Count == 0)
152 return;
153 _innerList.Clear();
154 RequestSave();
155 }
82 156 }
83 157
84 158 ///<inheritdoc/>
85 public bool Contains(TValue item) => ((ICollection<TValue>)_innerList).Contains(item);
159 public bool Contains(TValue item)
160 {
161 lock (_syncRoot)
162 return _innerList.Contains(item);
163 }
86 164
87 165 ///<inheritdoc/>
88 public bool Contains(object? value) => ((IList)_innerList).Contains(value);
166 public bool Contains(object? value)
167 {
168 lock (_syncRoot)
169 return ((IList)_innerList).Contains(value);
170 }
89 171
90 172 ///<inheritdoc/>
91 public void CopyTo(TValue[] array, int arrayIndex) => ((ICollection<TValue>)_innerList).CopyTo(array, arrayIndex);
173 public void CopyTo(TValue[] array, int arrayIndex)
174 {
175 lock (_syncRoot)
176 _innerList.CopyTo(array, arrayIndex);
177 }
92 178
93 179 ///<inheritdoc/>
94 public void CopyTo(Array array, int index) => ((ICollection)_innerList).CopyTo(array, index);
180 public void CopyTo(Array array, int index)
181 {
182 lock (_syncRoot)
183 ((ICollection)_innerList).CopyTo(array, index);
184 }
95 185
96 186 ///<inheritdoc/>
97 public IEnumerator<TValue> GetEnumerator() => ((IEnumerable<TValue>)_innerList).GetEnumerator();
187 public IEnumerator<TValue> GetEnumerator()
188 {
189 lock (_syncRoot)
190 return ((IEnumerable<TValue>)_innerList.ToArray()).GetEnumerator();
191 }
98 192
99 193 ///<inheritdoc/>
100 public int IndexOf(TValue item) => ((IList<TValue>)_innerList).IndexOf(item);
194 public int IndexOf(TValue item)
195 {
196 lock (_syncRoot)
197 return _innerList.IndexOf(item);
198 }
101 199
102 200 ///<inheritdoc/>
103 public int IndexOf(object? value) => ((IList)_innerList).IndexOf(value);
201 public int IndexOf(object? value)
202 {
203 lock (_syncRoot)
204 return ((IList)_innerList).IndexOf(value);
205 }
104 206
105 207 ///<inheritdoc/>
106 208 public void Insert(int index, TValue item)
107 209 {
108 ((IList<TValue>)_innerList).Insert(index, item);
109 CurrentProfile?.InstanceSaveProfile();
210 lock (_syncRoot)
211 {
212 _innerList.Insert(index, item);
213 RequestSave();
214 }
110 215 }
111 216
112 217 ///<inheritdoc/>
113 218 public void Insert(int index, object? value)
114 219 {
115 ((IList)_innerList).Insert(index, value);
116 CurrentProfile?.InstanceSaveProfile();
220 lock (_syncRoot)
221 {
222 ((IList)_innerList).Insert(index, value);
223 RequestSave();
224 }
117 225 }
118 226
119 227 ///<inheritdoc/>
120 228 public bool Remove(TValue item)
121 229 {
122 var result = ((ICollection<TValue>)_innerList).Remove(item);
123 CurrentProfile?.InstanceSaveProfile();
124 return result;
230 lock (_syncRoot)
231 {
232 var result = _innerList.Remove(item);
233 if (result)
234 RequestSave();
235 return result;
236 }
125 237 }
126 238
127 239 ///<inheritdoc/>
128 240 public void Remove(object? value)
129 241 {
130 ((IList)_innerList).Remove(value);
131 CurrentProfile?.InstanceSaveProfile();
242 lock (_syncRoot)
243 {
244 var count = _innerList.Count;
245 ((IList)_innerList).Remove(value);
246 if (_innerList.Count != count)
247 RequestSave();
248 }
132 249 }
133 250
134 251 ///<inheritdoc/>
135 252 public void RemoveAt(int index)
136 253 {
137 ((IList<TValue>)_innerList).RemoveAt(index);
138 CurrentProfile?.InstanceSaveProfile();
254 lock (_syncRoot)
255 {
256 _innerList.RemoveAt(index);
257 RequestSave();
258 }
259 }
260
261 IEnumerator IEnumerable.GetEnumerator()
262 {
263 lock (_syncRoot)
264 return _innerList.ToArray().GetEnumerator();
139 265 }
140 266
141 IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_innerList).GetEnumerator();
267 private void RequestSave() => _currentProfile?.InstanceRequestSaveProfile();
142 268 }
Modified XFEExtension.NetCore.AutoConfig/XFEExtension.NetCore.AutoConfig.csproj +1 -1
@@ -29,7 +29,7 @@
29 29
30 30 </PackageReleaseNotes>
31 31 <PackageLicenseFile>LICENSE.txt</PackageLicenseFile>
32 <Version>3.0.0</Version>
32 <Version>3.0.1</Version>
33 33 <GenerateDocumentationFile>True</GenerateDocumentationFile>
34 34 </PropertyGroup>
35 35
Modified XFEExtension.NetCore.AutoConfig/XFEProfile.cs +286 -22