返回提交历史
Modified
README.md
+21
-2
Modified
README.zh-CN.md
+21
-2
Modified
XFEExtension.NetCore.AutoConfig.Analyzer/Generator/ProfilePropertyAutoGenerator.cs
+56
-20
Added
XFEExtension.NetCore.AutoConfig.Tests/ConcurrentProfile.cs
+46
-0
Added
XFEExtension.NetCore.AutoConfig.Tests/ConcurrentSaveTests.cs
+88
-0
Added
XFEExtension.NetCore.AutoConfig.Tests/XFEExtension.NetCore.AutoConfig.Tests.csproj
+25
-0
Modified
XFEExtension.NetCore.AutoConfig.slnx
+1
-0
Modified
XFEExtension.NetCore.AutoConfig/ProfileDictionary.cs
+220
-45
Modified
XFEExtension.NetCore.AutoConfig/ProfileList.cs
+164
-38
Modified
XFEExtension.NetCore.AutoConfig/XFEExtension.NetCore.AutoConfig.csproj
+1
-1
Modified
XFEExtension.NetCore.AutoConfig/XFEProfile.cs
+286
-22
XFEstudio/XFEExtension.NetCore.AutoConfig
配置文件自动保存合并与线程安全支持
- ProfileList/Dictionary 增加内部锁,所有操作线程安全,集合变更合并自动保存 - XFEProfile 支持 AutoSaveDelay 合并窗口,后台保存异常暴露,采用原子写入 - 自动生成代码静态成员加锁,set 时自动请求保存 - 文档详细说明合并机制、并发安全、异常处理 - 新增并发保存合并等测试用例,验证正确性 - 版本号升级至 3.0.1
121cac6
代码差异
11 个文件
+929
-130
@@ -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
@@ -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
@@ -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}""/> 生成的导入配置文件的静态方法
@@ -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
}
@@ -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
}
@@ -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>
@@ -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>
@@ -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
}
@@ -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
}
@@ -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