├── tests ├── dotnetCampus.Configurations.Tests │ ├── configs.coin │ ├── configs.sim.coin │ ├── Fakes │ │ ├── FakeConfiguration.cs │ │ └── DebugConfiguration.cs │ ├── ConfigurationStringTests.cs │ ├── DefaultConfigurationTests.cs │ ├── dotnetCampus.Configurations.Tests.csproj │ ├── Utils │ │ └── TestUtil.cs │ └── ConfigurationTests.cs ├── dotnetCampus.Configurations.MicrosoftExtensionsConfiguration.Tests │ ├── appsettings.json │ ├── dotnetCampus.Configurations.MicrosoftExtensionsConfiguration.Tests.csproj │ └── MicrosoftConfigurationExtensions.cs └── dotnetCampus.Configurations.Benchmark │ ├── Program.cs │ ├── dotnetCampus.Configurations.Benchmark.csproj │ └── ConfigurationBenchmark.cs ├── src ├── dotnetCampus.Configurations │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── Concurrent │ │ ├── ChangeDetectionType.cs │ │ ├── FileEqualsComparison.cs │ │ ├── ICriticalReadWriteContext.cs │ │ ├── ProcessSafeValueEntry.cs │ │ ├── CriticalReadWriteContext.cs │ │ ├── ProcessSafeValueState.cs │ │ ├── TimedKeyValues.cs │ │ └── ProcessConcurrentDictionary.interface.cs │ ├── Core │ │ ├── RepoSyncingBehavior.cs │ │ ├── ConfigurationRepo.cs │ │ ├── IConfigurationRepo.cs │ │ ├── ConfigurationValueEntry.cs │ │ ├── ConcurrentAppConfigurator.cs │ │ ├── CommentedValue.cs │ │ ├── MemoryConfigurationRepo.cs │ │ ├── ConfigurationFactory.cs │ │ ├── CoinConfigurationSerializer.cs │ │ └── AsynchronousConfigurationRepo.cs │ ├── IAppConfigurator.cs │ ├── Utils │ │ ├── CT.cs │ │ ├── CollectionExtensions.cs │ │ ├── OSUtils.cs │ │ ├── WeakEventRelay.cs │ │ └── WeakEvent.cs │ ├── ClassNameUtils.cs │ ├── Threading │ │ ├── CountLimitOperationToken.cs │ │ ├── TimeoutOperationToken.cs │ │ ├── ContinuousPartOperation.cs │ │ └── PartialAwaitableRetry.cs │ ├── IO │ │ ├── FileSystemWatcherWeakEventRelay.cs │ │ └── FileWatcher.cs │ ├── Runtime │ │ └── OperationResult.cs │ ├── dotnetCampus.Configurations.csproj │ ├── ConfigurationString.cs │ ├── DefaultConfiguration.cs │ ├── Converters │ │ ├── ConfigurationStringExtensions.cs │ │ └── ConfigurationExtensions.cs │ ├── Extensions │ │ ├── CommandLineConfigurationProvider.cs │ │ └── CommandLineConfigurationExtensions.cs │ └── Configuration.cs ├── dotnetCampus.Configurations.MicrosoftExtensionsConfiguration │ ├── dotnetCampus.Configurations.MicrosoftExtensionsConfiguration.csproj │ ├── README.md │ ├── MicrosoftExtensionsConfigurationRepo.cs │ ├── MicrosoftConfigurationExtensions.cs │ └── MicrosoftExtensionsConfigurationBuildRepo.cs └── dotnetCampus.Configurations.WPFTypeConverter │ ├── dotnetCampus.Configurations.WPFTypeConverter.csproj │ └── Program.cs ├── LICENSE ├── Directory.Build.props ├── .github └── workflows │ ├── nuget publish.yml │ └── dotnetcore.yml ├── docs └── en-us │ └── README.md ├── .gitattributes ├── README.md ├── dotnetCampus.Configurations.sln └── .gitignore /tests/dotnetCampus.Configurations.Tests/configs.coin: -------------------------------------------------------------------------------- 1 | > 2 | Key 3 | Value 4 | > -------------------------------------------------------------------------------- /tests/dotnetCampus.Configurations.Tests/configs.sim.coin: -------------------------------------------------------------------------------- 1 | Test 2 | True 3 | > 4 | Value 5 | 1 6 | > 7 | Value 8 | 2 9 | > 10 | Result 11 | 3.141592653 12 | -------------------------------------------------------------------------------- /src/dotnetCampus.Configurations/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly:InternalsVisibleTo("dotnetCampus.Configurations.WPFTypeConverter")] 4 | [assembly: InternalsVisibleTo("dotnetCampus.Configurations.Tests")] -------------------------------------------------------------------------------- /tests/dotnetCampus.Configurations.MicrosoftExtensionsConfiguration.Tests/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } -------------------------------------------------------------------------------- /tests/dotnetCampus.Configurations.Benchmark/Program.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Running; 2 | using System; 3 | using System.Reflection; 4 | 5 | namespace dotnetCampus.Configurations.Benchmark 6 | { 7 | class Program 8 | { 9 | static void Main(string[] args) 10 | { 11 | BenchmarkRunner.Run(); 12 | //BenchmarkSwitcher.FromAssembly(typeof(Program).GetTypeInfo().Assembly).Run(args); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/dotnetCampus.Configurations/Concurrent/ChangeDetectionType.cs: -------------------------------------------------------------------------------- 1 | namespace dotnetCampus.Configurations.Concurrent 2 | { 3 | /// 4 | /// 表示如何识别文件是否改变。 5 | /// 6 | internal enum ChangeDetectionType 7 | { 8 | /// 9 | /// 整个文件中有必须所有内容相同才视为相同。 10 | /// 11 | WholeTextEquals, 12 | 13 | /// 14 | /// 无视文件内容,只要新旧文件中包含的键值集合无序相等,则视为相同。 15 | /// 16 | KeyValueEquals, 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/dotnetCampus.Configurations/Concurrent/FileEqualsComparison.cs: -------------------------------------------------------------------------------- 1 | namespace dotnetCampus.Configurations.Concurrent 2 | { 3 | /// 4 | /// 表示如何表示文件内容是否相等。 5 | /// 6 | internal enum FileEqualsComparison 7 | { 8 | /// 9 | /// 整个文件中必须所有内容相同才视为相等。 10 | /// 11 | WholeTextEquals, 12 | 13 | /// 14 | /// 无视文件内容,只要新旧文件中包含的键值集合无序相等,则视为相等。 15 | /// 16 | KeyValueEquals, 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/dotnetCampus.Configurations/Core/RepoSyncingBehavior.cs: -------------------------------------------------------------------------------- 1 | namespace dotnetCampus.Configurations.Core 2 | { 3 | /// 4 | /// 表示配置仓库应如何与外部数据进行同步。 5 | /// 6 | public enum RepoSyncingBehavior 7 | { 8 | /// 9 | /// 以高效的方式进行同步。 10 | /// 11 | /// 12 | /// 使用此方式会导致仓库监听外部文件的改变。 13 | /// 14 | Sync, 15 | 16 | /// 17 | /// 不进行同步,首次读到值后将不再自动根据外部值更新数据,除非手动调用方法更新数据。 18 | /// 19 | Static, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/dotnetCampus.Configurations.Benchmark/dotnetCampus.Configurations.Benchmark.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp3.1 6 | false 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /tests/dotnetCampus.Configurations.Tests/Fakes/FakeConfiguration.cs: -------------------------------------------------------------------------------- 1 | namespace dotnetCampus.Configurations.Tests.Fakes 2 | { 3 | internal class FakeConfiguration : Configuration 4 | { 5 | public FakeConfiguration() : base("") 6 | { 7 | } 8 | 9 | public string Key 10 | { 11 | get => GetString(); 12 | set => SetValue(value); 13 | } 14 | 15 | public string A 16 | { 17 | get => GetString(); 18 | set => SetValue(value); 19 | } 20 | 21 | public string B 22 | { 23 | get => GetString(); 24 | set => SetValue(value); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/dotnetCampus.Configurations.MicrosoftExtensionsConfiguration/dotnetCampus.Configurations.MicrosoftExtensionsConfiguration.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 1.0.0-alpha01 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/dotnetCampus.Configurations/Core/ConfigurationRepo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace dotnetCampus.Configurations.Core 4 | { 5 | /// 6 | /// 提供一个同步 配置管理仓库的基类。 7 | /// 8 | public abstract class ConfigurationRepo : IConfigurationRepo 9 | { 10 | /// 11 | public IAppConfigurator CreateAppConfigurator() => new ConcurrentAppConfigurator(this); 12 | 13 | /// 14 | public abstract string? GetValue(string key); 15 | 16 | /// 17 | public abstract void SetValue(string key, string? value); 18 | 19 | /// 20 | public abstract void ClearValues(Predicate keyFilter); 21 | } 22 | } -------------------------------------------------------------------------------- /src/dotnetCampus.Configurations/IAppConfigurator.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CA1716 2 | 3 | namespace dotnetCampus.Configurations 4 | { 5 | /// 6 | /// 管理应用程序中 类型的配置项。 7 | /// 8 | public interface IAppConfigurator 9 | { 10 | /// 11 | /// 获取 类型的配置项组。 12 | /// 13 | /// 配置项组的类型。 14 | /// 配置项组。 15 | TConfiguration Of() where TConfiguration : Configuration, new(); 16 | 17 | /// 18 | /// 获取默认的纯字符串值的配置项组。 19 | /// 20 | DefaultConfiguration Default { get; } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/dotnetCampus.Configurations/Utils/CT.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Linq; 4 | 5 | namespace dotnetCampus.Configurations.Utils 6 | { 7 | /// 8 | /// 配置组件专用的日志记录器。 9 | /// 10 | internal static class CT 11 | { 12 | /// 13 | /// 记一个日志。 14 | /// 15 | /// 消息。 16 | /// 消息标签(用于分类)。 17 | [Conditional("UNITTEST")] 18 | internal static void Log(string message, params string[] tags) 19 | { 20 | var text = $"[{DateTime.Now:O}] {string.Join(" ", tags.Select(x => $"[{x}]"))} {message}"; 21 | Console.WriteLine(text); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/dotnetCampus.Configurations.MicrosoftExtensionsConfiguration/README.md: -------------------------------------------------------------------------------- 1 | # dotnetCampus.Configurations.MicrosoftExtensionsConfiguration 2 | 3 | 兼容 Microsoft.Extensions.Configuration 的逻辑,可以将 Microsoft.Extensions.Configuration 的配置加入到 dotnetCampus.Configurations 里面 4 | 5 | ## 使用方法 6 | 7 | 可以对 IConfigurationBuilder 和 IConfiguration 对象调用 ToAppConfigurator 方法进行接入 8 | 9 | ```csharp 10 | public void Foo(IConfigurationBuilder builder) 11 | { 12 | IAppConfigurator appConfigurator = builder.ToAppConfigurator(); 13 | // 完成接入 14 | } 15 | 16 | public void Foo(IConfiguration configuration) 17 | { 18 | IAppConfigurator appConfigurator = configuration.ToAppConfigurator(); 19 | // 完成接入 20 | } 21 | ``` 22 | 23 | 在获取到 IAppConfigurator 对象之后,就可以使用 dotnetCampus.Configurations 的方式读写配置 -------------------------------------------------------------------------------- /tests/dotnetCampus.Configurations.Tests/ConfigurationStringTests.cs: -------------------------------------------------------------------------------- 1 | using dotnetCampus.Configurations.Utils; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using MSTest.Extensions.Contracts; 4 | 5 | namespace dotnetCampus.Configurations.Tests 6 | { 7 | [TestClass] 8 | public class ConfigurationStringTests 9 | { 10 | [ContractTestCase] 11 | public void Convert() 12 | { 13 | "支持从 string 隐式转换为 ConfigurationString? 类".Test(() => 14 | { 15 | // Arrange 16 | string value = "lindexi"; 17 | 18 | // Act 19 | ConfigurationString? configurationString = value; 20 | 21 | // Assert 22 | Assert.AreEqual(true, configurationString != null); 23 | }); 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /src/dotnetCampus.Configurations/Concurrent/ICriticalReadWriteContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace dotnetCampus.Configurations.Concurrent 5 | { 6 | /// 7 | /// 为跨进程的键值对的读写提供安全的读写上下文。 8 | /// 9 | /// 键。 10 | /// 值。 11 | public interface ICriticalReadWriteContext 12 | { 13 | /// 14 | /// 在进程安全的上下文中,当读到键值集合后请调用此方法将值传入以得到合并后的键值集合。 15 | /// 16 | /// 在进程安全的上下文中读到的键值集合。 17 | /// 外部值的最近更新时间。 18 | public TimedKeyValues MergeExternalKeyValues(IReadOnlyDictionary keyValues, DateTimeOffset? externalUpdateTime = null); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/dotnetCampus.Configurations.Tests/DefaultConfigurationTests.cs: -------------------------------------------------------------------------------- 1 | using dotnetCampus.Configurations.Tests.Utils; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using MSTest.Extensions.Contracts; 4 | 5 | namespace dotnetCampus.Configurations.Tests 6 | { 7 | [TestClass] 8 | public class DefaultConfigurationTests 9 | { 10 | [ContractTestCase] 11 | public void FromFile() 12 | { 13 | "从文件获取一个默认的配置,可以得到配置。".Test(() => 14 | { 15 | // Arrange 16 | var coin = TestUtil.GetTempFile("configs.sim.coin"); 17 | 18 | // Act 19 | var configs = DefaultConfiguration.FromFile(coin.FullName); 20 | 21 | // Assert 22 | var value = configs["Foo"]; 23 | Assert.IsFalse(value.HasValue); 24 | }); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/dotnetCampus.Configurations/Utils/CollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace dotnetCampus.Configurations.Utils 6 | { 7 | internal static class CollectionExtensions 8 | { 9 | /// 10 | /// 在忽略顺序的情况下,比较两个集合是否相等(长度相等,包含的元素相等)。 11 | /// 12 | /// 元素类型。 13 | /// 集合 1。 14 | /// 集合 2。 15 | /// 如果两个集合忽略顺序后相等,则返回 true,否则返回 false。 16 | internal static bool SequenceEqualsIgnoringOrder(this ICollection list1, ICollection list2) 17 | { 18 | if (list1 == null) 19 | { 20 | throw new ArgumentNullException(nameof(list1)); 21 | } 22 | 23 | if (list2 == null) 24 | { 25 | throw new ArgumentNullException(nameof(list2)); 26 | } 27 | 28 | return list1.Count == list2.Count && list1.All(list2.Contains); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2025 dotnet-campus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/dotnetCampus.Configurations/ClassNameUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace dotnetCampus.Configurations 4 | { 5 | /// 6 | /// 包含类名相关的处理方法。 7 | /// 8 | internal static class ClassNameUtils 9 | { 10 | /// 11 | /// 当某个类型的派生类都以基类()名称作为后缀时,去掉后缀取派生类名称的前面部分。 12 | /// 13 | /// 名称统一的基类名称。 14 | /// 派生类的实例。 15 | /// 去掉后缀的派生类名称。 16 | internal static string GetClassNameWithoutSuffix(this T @this) 17 | { 18 | if (@this is null) 19 | { 20 | throw new ArgumentNullException(nameof(@this)); 21 | } 22 | 23 | var derivedTypeName = @this.GetType().Name; 24 | var baseTypeName = typeof(T).Name; 25 | // 截取子类名称中去掉基类后缀的部分。 26 | var name = derivedTypeName.EndsWith(baseTypeName, StringComparison.Ordinal) 27 | ? derivedTypeName.Substring(0, derivedTypeName.Length - baseTypeName.Length) 28 | : derivedTypeName; 29 | // 如果子类名称和基类完全一样,则直接返回子类名称。 30 | return string.IsNullOrWhiteSpace(name) ? derivedTypeName : name; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | $(MSBuildThisFileDirectory)bin\$(Configuration) 6 | true 7 | walterlv 8 | dotnet-campus 9 | MIT 10 | https://github.com/dotnet-campus/dotnetCampus.Configurations 11 | https://github.com/dotnet-campus/dotnetCampus.Configurations.git 12 | git 13 | latest 14 | enable 15 | Copyright (c) dotnet-campus 2020-$([System.DateTime]::Now.ToString(`yyyy`)) 16 | README.md 17 | 18 | true 19 | 20 | true 21 | snupkg 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/dotnetCampus.Configurations/Core/IConfigurationRepo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace dotnetCampus.Configurations.Core 4 | { 5 | /// 6 | /// 管理应用程序中的字符串配置项。 7 | /// 8 | public interface IConfigurationRepo 9 | { 10 | /// 11 | /// 创建一个使用强类型的用于提供给应用程序业务使用的应用程序配置管理器。 12 | /// 13 | /// 用于提供给应用程序业务使用的配置管理器。 14 | IAppConfigurator CreateAppConfigurator(); 15 | 16 | /// 17 | /// 获取指定配置项的值,如果指定的 不存在,则返回 null。 18 | /// 此方法是线程安全的。 19 | /// 20 | /// 配置项的标识符。 21 | /// 配置项的值。 22 | string? GetValue(string key); 23 | 24 | /// 25 | /// 设置指定配置项的值,如果设置为 null,可能删除 配置项。 26 | /// 此方法是线程安全的。 27 | /// 28 | /// 配置项的标识符。 29 | /// 配置项的值。 30 | void SetValue(string key, string? value); 31 | 32 | /// 33 | /// 删除所有满足 规则的 Key 所表示的配置项。 34 | /// 35 | /// 36 | /// 指定如何过滤 Key。当指定为 null 时,全部清除。 37 | /// 38 | void ClearValues(Predicate keyFilter); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/dotnetCampus.Configurations/Concurrent/ProcessSafeValueEntry.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | 4 | namespace dotnetCampus.Configurations.Concurrent 5 | { 6 | /// 7 | /// 表示一个跨进程安全的值存储空间。 8 | /// 9 | /// 值的类型。 10 | [DebuggerDisplay("{Value,nq} [{State}] // {LastUpdateTime}")] 11 | internal readonly struct ProcessSafeValueEntry 12 | { 13 | internal ProcessSafeValueEntry(TValue value, TValue externalValue, DateTimeOffset timestamp, ProcessSafeValueState state) 14 | { 15 | Value = value; 16 | ExternalValue = externalValue; 17 | LastUpdateTime = timestamp; 18 | State = state; 19 | } 20 | 21 | /// 22 | /// 获取此值的最新修改时间。如果值是外部修改的,则为外部文件的最近写时间;如果值是内部修改的,则为最近赋值的时间。 23 | /// 24 | public DateTimeOffset LastUpdateTime { get; } 25 | 26 | /// 27 | /// 获取此值在外部存储中的值。 28 | /// 29 | public TValue ExternalValue { get; } 30 | 31 | /// 32 | /// 获取此值当前的最新值。如果尚未修改,它一定与外部值 相等。 33 | /// 34 | public TValue Value { get; } 35 | 36 | /// 37 | /// 获取此值在内存中相比于外部存储来说的修改状态。 38 | /// 39 | public ProcessSafeValueState State { get; } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/dotnetCampus.Configurations/Concurrent/CriticalReadWriteContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace dotnetCampus.Configurations.Concurrent 5 | { 6 | /// 7 | /// 为跨进程的键值对的读写提供安全的读写上下文。 8 | /// 9 | /// 键。 10 | /// 值。 11 | internal class CriticalReadWriteContext : ICriticalReadWriteContext 12 | { 13 | private readonly Func, DateTimeOffset, TimedKeyValues> _keyValueMergingFunc; 14 | 15 | /// 16 | /// 创建 的新实例。 17 | /// 18 | /// 请在此委托中合并键值集合。 19 | public CriticalReadWriteContext(Func, DateTimeOffset, 20 | TimedKeyValues> keyValueMergingFunc) 21 | { 22 | _keyValueMergingFunc = keyValueMergingFunc ?? throw new ArgumentNullException(nameof(keyValueMergingFunc)); 23 | } 24 | 25 | /// 26 | public TimedKeyValues MergeExternalKeyValues(IReadOnlyDictionary keyValues, DateTimeOffset? externalUpdateTime = null) 27 | => _keyValueMergingFunc(keyValues, externalUpdateTime ?? DateTimeOffset.UtcNow); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/dotnetCampus.Configurations/Utils/OSUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Runtime.InteropServices; 5 | 6 | namespace dotnetCampus.Configurations.Utils 7 | { 8 | internal static class OSUtils 9 | { 10 | /// 11 | /// 判断当前系统环境下两个路径是否是同一个路径。 12 | /// 注意,对于相对路径以及路径的 ./.. 跳转,即使最终对应同一个文件,也不被视为相同路径。 13 | /// 14 | /// 路径 1。 15 | /// 路径 2。 16 | /// 如果当前系统环境下是同一个路径则返回 true,否则返回 false。 17 | internal static bool PathEquals(string path1, string path2) 18 | { 19 | return string.Equals(path1, path2, 20 | IsPathCaseSensitive() ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase); 21 | } 22 | 23 | /// 24 | /// 判断当前系统环境下路径是否是大小写敏感的。 25 | /// 26 | /// 如果当前系统环境下路径大小写敏感则返回 true,否则返回 false。 27 | internal static bool IsPathCaseSensitive() 28 | { 29 | #if NETFRAMEWORK 30 | return false; 31 | #else 32 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) 33 | || RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) 34 | { 35 | return false; 36 | } 37 | return true; 38 | #endif 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /.github/workflows/nuget publish.yml: -------------------------------------------------------------------------------- 1 | name: NuGet Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | build: 10 | runs-on: windows-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v1 14 | 15 | - name: Install dotnet tool 16 | run: dotnet tool install -g dotnetCampus.TagToVersion 17 | 18 | - name: Set tag to version 19 | run: cmd /c "dotnet TagToVersion -t ${{ github.ref }}" 20 | 21 | - name: Setup .NET 22 | uses: actions/setup-dotnet@v4 23 | with: 24 | dotnet-version: | 25 | 3.1.x 26 | 5.0.x 27 | 6.0.x 28 | 7.0.x 29 | 8.0.x 30 | 9.0.x 31 | 32 | - name: Build with dotnet 33 | run: dotnet build --configuration Release -v n 34 | shell: pwsh 35 | 36 | - name: Install Nuget 37 | uses: nuget/setup-nuget@v1 38 | with: 39 | nuget-version: '5.x' 40 | 41 | - name: Add private GitHub registry to NuGet 42 | run: | 43 | nuget sources add -name github -Source https://nuget.pkg.github.com/dotnet-campus/index.json -Username dotnet-campus -Password ${{ secrets.GITHUB_TOKEN }} 44 | 45 | - name: Push generated package to GitHub registry 46 | run: | 47 | nuget push .\bin\release\*.nupkg -Source github -SkipDuplicate 48 | nuget push .\bin\release\*.nupkg -Source https://api.nuget.org/v3/index.json -SkipDuplicate -ApiKey ${{ secrets.NugetKey }} 49 | -------------------------------------------------------------------------------- /src/dotnetCampus.Configurations.MicrosoftExtensionsConfiguration/MicrosoftExtensionsConfigurationRepo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using dotnetCampus.Configurations.Core; 4 | using Microsoft.Extensions.Configuration; 5 | 6 | namespace dotnetCampus.Configurations.MicrosoftExtensionsConfiguration 7 | { 8 | internal class MicrosoftExtensionsConfigurationRepo : ConfigurationRepo 9 | { 10 | public MicrosoftExtensionsConfigurationRepo(IConfiguration configuration) 11 | { 12 | Configuration = configuration; 13 | } 14 | 15 | public override string? GetValue(string key) 16 | { 17 | return Configuration[key]; 18 | } 19 | 20 | public override void SetValue(string key, string? value) 21 | { 22 | Configuration[key] = value; 23 | } 24 | 25 | public override void ClearValues(Predicate keyFilter) 26 | { 27 | var removeList = new List(); 28 | foreach (var (key, _) in Configuration.AsEnumerable()) 29 | { 30 | if (keyFilter(key)) 31 | { 32 | removeList.Add(key); 33 | } 34 | } 35 | 36 | foreach (var key in removeList) 37 | { 38 | SetValue(key, null); 39 | } 40 | } 41 | 42 | private IConfiguration Configuration { get; } 43 | } 44 | } -------------------------------------------------------------------------------- /.github/workflows/dotnetcore.yml: -------------------------------------------------------------------------------- 1 | name: .NET Core 2 | 3 | on: [push] 4 | 5 | jobs: 6 | BuildAndTestOnWindows: 7 | 8 | runs-on: windows-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: Setup .NET 13 | uses: actions/setup-dotnet@v1 14 | with: 15 | dotnet-version: | 16 | 3.1.x 17 | 5.0.x 18 | 6.0.100 19 | 9.0.x 20 | - name: Build with dotnet 21 | run: dotnet build --configuration Release 22 | - name: Test 23 | run: dotnet test "tests/dotnetCampus.Configurations.Tests" --configuration UnitTest -l "console;verbosity=detailed" 24 | 25 | TestOnLinux: 26 | 27 | runs-on: ubuntu-latest 28 | 29 | steps: 30 | - uses: actions/checkout@v1 31 | - name: Setup .NET 32 | uses: actions/setup-dotnet@v1 33 | with: 34 | dotnet-version: | 35 | 3.1.x 36 | 5.0.x 37 | 6.0.100 38 | 9.0.x 39 | - name: Test 40 | run: dotnet test "tests/dotnetCampus.Configurations.Tests" --configuration Release -f net9.0 41 | 42 | TestOnMac: 43 | 44 | runs-on: macos-latest 45 | 46 | steps: 47 | - uses: actions/checkout@v1 48 | - name: Setup .NET 49 | uses: actions/setup-dotnet@v1 50 | with: 51 | dotnet-version: | 52 | 3.1.x 53 | 5.0.x 54 | 6.0.100 55 | 9.0.x 56 | - name: Test 57 | run: dotnet test "tests/dotnetCampus.Configurations.Tests" --configuration Release -f net9.0 -------------------------------------------------------------------------------- /src/dotnetCampus.Configurations/Concurrent/ProcessSafeValueState.cs: -------------------------------------------------------------------------------- 1 | namespace dotnetCampus.Configurations.Concurrent 2 | { 3 | /// 4 | /// 表示跨进程安全的值的状态。 5 | /// 6 | internal enum ProcessSafeValueState 7 | { 8 | /// 9 | /// 从外部(文件)读到值时,会保持此值。 10 | /// 11 | /// 被修改后会改为 12 | /// 被删除后会改为 13 | /// 14 | /// 15 | NotChanged = 0, 16 | 17 | ///// 18 | ///// 由此进程新建的值会保持此值。 19 | ///// 20 | ///// 与外部文件同步后会改为 21 | ///// 被删除后会改为 22 | ///// 即使被修改,也不会改为 23 | ///// 24 | ///// 25 | //New = 1, 26 | 27 | /// 28 | /// 由本进程修改过的值会保持此值。 29 | /// 30 | /// 与外部文件同步后会改为 31 | /// 被删除后会改为 32 | /// 33 | /// 34 | Changed = 2, 35 | 36 | /// 37 | /// 由本进程删除的值会保持此值。 38 | /// 39 | /// 与外部文件同步后会根据外部值是否改变决定是真实删除值还是读取外部值后改为 40 | /// 修改后会改为 41 | /// 42 | /// 43 | Deleted = 3, 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/dotnetCampus.Configurations.Tests/dotnetCampus.Configurations.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net48;net9.0 5 | latest 6 | enable 7 | false 8 | true 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /tests/dotnetCampus.Configurations.MicrosoftExtensionsConfiguration.Tests/dotnetCampus.Configurations.MicrosoftExtensionsConfiguration.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | false 7 | false 8 | true 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | all 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | PreserveNewest 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /tests/dotnetCampus.Configurations.Benchmark/ConfigurationBenchmark.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Attributes; 2 | using dotnetCampus.Configurations.Core; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace dotnetCampus.Configurations.Benchmark 10 | { 11 | public class ConfigurationBenchmark 12 | { 13 | [Benchmark(Baseline = true, Description = "仅内存缓存")] 14 | public void ReadDirectly() 15 | { 16 | const string dcc = "configs.01.dcc"; 17 | FileConfigurationRepo repo = ConfigurationFactory.FromFile(dcc); 18 | var configs = repo.CreateAppConfigurator().Of(); 19 | _ = configs.Key; 20 | } 21 | 22 | [Benchmark(Description = "先检查文件")] 23 | public async Task ReadWithFileChangeChecking() 24 | { 25 | const string dcc = "configs.02.dcc"; 26 | FileConfigurationRepo repo = ConfigurationFactory.FromFile(dcc); 27 | var configs = repo.CreateAppConfigurator().Of(); 28 | await repo.ReloadExternalChangesAsync().ConfigureAwait(false); 29 | _ = configs.Key; 30 | } 31 | 32 | private class FakeConfiguration : Configuration 33 | { 34 | public FakeConfiguration() : base("") 35 | { 36 | } 37 | 38 | public string Key 39 | { 40 | get => GetString(); 41 | set => SetValue(value); 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/dotnetCampus.Configurations/Core/ConfigurationValueEntry.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace dotnetCampus.Configurations.Core 4 | { 5 | internal readonly struct ConfigurationValueEntry 6 | { 7 | public ConfigurationValueEntry(string value) 8 | { 9 | Value = value; 10 | _updatedTime = DateTimeOffset.Now; 11 | } 12 | 13 | public string Value { get; } 14 | 15 | private readonly DateTimeOffset _updatedTime; 16 | 17 | public static ConfigurationValueEntry GetLatest(ConfigurationValueEntry value1, ConfigurationValueEntry value2) 18 | => value1._updatedTime > value2._updatedTime ? value1 : value2; 19 | 20 | [Obsolete("不含时间戳的字符串无法当作配置项比较新旧。", true)] 21 | public static ConfigurationValueEntry GetLatest(string value1, string value2) 22 | => throw new NotSupportedException("不含时间戳的字符串无法当作配置项比较新旧。"); 23 | 24 | [Obsolete("不含时间戳的字符串无法当作配置项比较新旧。", true)] 25 | public static ConfigurationValueEntry GetLatest(string value1, ConfigurationValueEntry value2) 26 | => throw new NotSupportedException("不含时间戳的字符串无法当作配置项比较新旧。"); 27 | 28 | [Obsolete("不含时间戳的字符串无法当作配置项比较新旧。", true)] 29 | public static ConfigurationValueEntry GetLatest(ConfigurationValueEntry value1, string value2) 30 | => throw new NotSupportedException("不含时间戳的字符串无法当作配置项比较新旧。"); 31 | 32 | public static implicit operator ConfigurationValueEntry(string value) => new ConfigurationValueEntry(value); 33 | public static implicit operator string(ConfigurationValueEntry entry) => entry.Value; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/dotnetCampus.Configurations.WPFTypeConverter/dotnetCampus.Configurations.WPFTypeConverter.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1;net45 5 | true 6 | true 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | true 17 | 18 | true 19 | 20 | 21 | 22 | 23 | 24 | true 25 | 26 | 27 | 28 | true 29 | snupkg 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /docs/en-us/README.md: -------------------------------------------------------------------------------- 1 | # COIN Configuration 2 | 3 | COIN = Configuration\n , means `configuration + Line break`, that is the reason for its naming. COIN has designed a high-performance application configuration file and a .NET library for high-performance reading and writing of this configuration file 4 | 5 | |Build|NuGet| 6 | |--|--| 7 | |![](https://github.com/dotnet-campus/dotnetCampus.Configurations/workflows/.NET%20Core/badge.svg)|[![](https://img.shields.io/nuget/v/dotnetCampus.Configurations.svg)](https://www.nuget.org/packages/dotnetCampus.Configurations)| 8 | 9 | ## Configuration file format 10 | 11 | The configuration file is in units of lines, and the line with the character of `>` at the beginning of the line is used as a comment, and the content after `>` will be ignored. The line beginning with the first non- `>` character is used as the `Key` value, and the content between the lines below this line until the end of the file or the beginning of the next `>` character is used as the `Value` value 12 | 13 | ``` 14 | > COIN Configuration 15 | > Version 1.0 16 | State.BuildLogFile 17 | xxxxx 18 | > Comment content 19 | Foo 20 | This is the first line 21 | This is the second line 22 | > 23 | > End Configuration 24 | ``` 25 | 26 | ## Install NuGet 27 | 28 | ``` 29 | dotnet add package dotnetCampus.Configurations 30 | ``` 31 | 32 | ## Usage 33 | 34 | Initialization: 35 | 36 | ```csharp 37 | var configs = DefaultConfiguration.FromFile(@"C:\Users\lvyi\Desktop\walterlv.fkv"); 38 | ``` 39 | 40 | GetValue: 41 | 42 | ```csharp 43 | string value0 = configs["Foo"]; 44 | 45 | string value1 = configs["Foo"] ?? "anonymous"; 46 | ``` 47 | 48 | SetValue: 49 | 50 | ```csharp 51 | configs["Foo"] = "lvyi"; 52 | 53 | configs["Foo"] = null; 54 | 55 | configs["Foo"] = ""; 56 | ``` 57 | -------------------------------------------------------------------------------- /src/dotnetCampus.Configurations/Core/ConcurrentAppConfigurator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | 5 | namespace dotnetCampus.Configurations.Core 6 | { 7 | /// 8 | /// 以线程安全的方式管理应用程序中 类型的配置项。 9 | /// 10 | internal sealed class ConcurrentAppConfigurator : IAppConfigurator 11 | { 12 | /// 13 | /// 创建使用 管理的线程安全的应用程序配置。 14 | /// 15 | /// 配置管理器。 16 | internal ConcurrentAppConfigurator(IConfigurationRepo repo) 17 | { 18 | _repo = repo ?? throw new ArgumentNullException(nameof(repo)); 19 | } 20 | 21 | private readonly IConfigurationRepo _repo; 22 | 23 | private readonly ConcurrentDictionary _configurationDictionary 24 | = new ConcurrentDictionary(); 25 | 26 | /// 27 | /// 获取 类型的配置项组。 28 | /// 29 | /// 配置项组的类型。 30 | /// 配置项组。 31 | public TConfiguration Of() where TConfiguration : Configuration, new() 32 | { 33 | return (TConfiguration) _configurationDictionary.GetOrAdd(typeof(TConfiguration), _ => 34 | { 35 | return new TConfiguration 36 | { 37 | Repo = _repo, 38 | AppConfigurator = this, 39 | }; 40 | }); 41 | } 42 | 43 | /// 44 | /// 获取默认的纯字符串值的配置项组。 45 | /// 46 | public DefaultConfiguration Default => Of(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/dotnetCampus.Configurations.MicrosoftExtensionsConfiguration/MicrosoftConfigurationExtensions.cs: -------------------------------------------------------------------------------- 1 | using dotnetCampus.Configurations; 2 | using dotnetCampus.Configurations.MicrosoftExtensionsConfiguration; 3 | 4 | // 这里特别设置为 Microsoft.Extensions 命名空间,用于解决引用的时候使用扩展方法需要加上一堆命名空间 5 | // ReSharper disable once CheckNamespace 6 | namespace Microsoft.Extensions.Configuration 7 | { 8 | /// 9 | /// 配置的扩展方法,用于将 Microsoft.Extensions.Configuration 接入到 dotnetCampus.Configurations 库 10 | /// 11 | public static class MicrosoftConfigurationExtensions 12 | { 13 | /// 14 | /// 将 转换为 的方法 15 | /// 16 | /// 17 | /// 18 | public static IAppConfigurator ToAppConfigurator(this IConfiguration configuration) => 19 | new MicrosoftExtensionsConfigurationRepo(configuration).CreateAppConfigurator(); 20 | 21 | /// 22 | /// 将 转换为 的方法 23 | /// 24 | /// 25 | /// 26 | public static IAppConfigurator ToAppConfigurator(this IConfigurationBuilder configuration) => 27 | new MicrosoftExtensionsConfigurationBuildRepo(configuration).CreateAppConfigurator(); 28 | 29 | /// 30 | public static IAppConfigurator ToAppConfigurator(this T configuration) 31 | where T : IConfiguration, IConfigurationBuilder 32 | { 33 | IConfigurationBuilder configurationBuilder = configuration; 34 | return configurationBuilder.ToAppConfigurator(); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/dotnetCampus.Configurations/Core/CommentedValue.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Runtime.CompilerServices; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace dotnetCampus.Configurations.Core 10 | { 11 | /// 12 | /// 被注释的值。 13 | /// 14 | [DebuggerDisplay("{Value,nq} // {Comment,nq}")] 15 | public readonly struct CommentedValue : IEquatable> 16 | { 17 | /// 18 | /// 值。 19 | /// 20 | public T Value { get; } 21 | 22 | /// 23 | /// 注释。 24 | /// 25 | public string Comment { get; } 26 | 27 | public CommentedValue(T value, string comment = "") 28 | { 29 | Value = value; 30 | Comment = comment ?? throw new ArgumentNullException(nameof(comment)); 31 | } 32 | 33 | public override bool Equals(object? obj) 34 | { 35 | return obj is CommentedValue value && Equals(value); 36 | } 37 | 38 | public bool Equals(CommentedValue other) 39 | { 40 | return EqualityComparer.Default.Equals(Value, other.Value) 41 | && string.Equals(Comment, other.Comment, StringComparison.Ordinal); 42 | } 43 | 44 | public override int GetHashCode() 45 | { 46 | var hashCode = 618185720; 47 | hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Value); 48 | hashCode = hashCode * -1521134295 + StringComparer.Ordinal.GetHashCode(Comment); 49 | return hashCode; 50 | } 51 | 52 | public static bool operator ==(CommentedValue left, CommentedValue right) => left.Equals(right); 53 | 54 | public static bool operator !=(CommentedValue left, CommentedValue right) => !(left == right); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/dotnetCampus.Configurations/Concurrent/TimedKeyValues.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace dotnetCampus.Configurations.Concurrent 5 | { 6 | /// 7 | /// 包含时间戳的键值集合。 8 | /// 9 | public readonly struct TimedKeyValues : IEquatable> 10 | { 11 | /// 12 | /// 创建 的新实例。 13 | /// 14 | /// 键值集合。 15 | /// 键值集合的更新时间。 16 | public TimedKeyValues(IReadOnlyDictionary keyValues, DateTimeOffset time) 17 | { 18 | KeyValues = keyValues ?? throw new ArgumentNullException(nameof(keyValues)); 19 | Time = time; 20 | } 21 | 22 | /// 23 | /// 键值集合。 24 | /// 25 | public IReadOnlyDictionary KeyValues { get; } 26 | 27 | /// 28 | /// 键值集合的更新时间。 29 | /// 30 | public DateTimeOffset Time { get; } 31 | 32 | public override bool Equals(object? obj) 33 | { 34 | return obj is TimedKeyValues values && Equals(values); 35 | } 36 | 37 | public bool Equals(TimedKeyValues other) 38 | { 39 | return EqualityComparer>.Default.Equals(KeyValues, other.KeyValues) && 40 | Time.Equals(other.Time); 41 | } 42 | 43 | public override int GetHashCode() 44 | { 45 | var hashCode = -746944874; 46 | hashCode = hashCode * -1521134295 + EqualityComparer>.Default.GetHashCode(KeyValues); 47 | hashCode = hashCode * -1521134295 + Time.GetHashCode(); 48 | return hashCode; 49 | } 50 | 51 | public static bool operator ==(TimedKeyValues left, TimedKeyValues right) => left.Equals(right); 52 | public static bool operator !=(TimedKeyValues left, TimedKeyValues right) => !(left == right); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/dotnetCampus.Configurations.Tests/Utils/TestUtil.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | using System; 3 | using System.Collections.Concurrent; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Reflection; 7 | 8 | namespace dotnetCampus.Configurations.Tests.Utils 9 | { 10 | /// 11 | /// 为需要清理的单元测试提供辅助工具。 12 | /// 13 | [TestClass] 14 | public static class TestUtil 15 | { 16 | private static readonly ConcurrentDictionary AllTempFiles = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); 17 | 18 | /// 19 | /// 获取一个临时的用于测试的文件。 20 | /// 21 | /// 指定临时文件应该使用的扩展名。 22 | /// 23 | /// 如果指定临时文件的模板,则会确保生成的临时文件存在且与模板文件相同; 24 | /// 如果指定临时文件的模板为 null,则仅会返回一个临时文件的路径,而不会创建文件。 25 | /// 将此配置文件放入到某文件夹中。 26 | /// 用于测试的临时文件。 27 | public static FileInfo GetTempFile(string? templateFileName = null, string? extension = null, string? relativeFilePath = null) 28 | { 29 | var newFileName = Path.Combine( 30 | Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, 31 | relativeFilePath ?? "", 32 | Path.GetFileNameWithoutExtension(Path.GetTempFileName())); 33 | if (!string.IsNullOrWhiteSpace(templateFileName)) 34 | { 35 | extension = extension ?? Path.GetExtension(templateFileName); 36 | newFileName += extension; 37 | File.Copy(templateFileName, newFileName); 38 | } 39 | else 40 | { 41 | newFileName += extension; 42 | } 43 | var fileInfo = new FileInfo(newFileName); 44 | AllTempFiles[newFileName] = fileInfo; 45 | return fileInfo; 46 | } 47 | 48 | [AssemblyCleanup] 49 | public static void CleanupTempFiles() 50 | { 51 | foreach (var file in AllTempFiles.Where(x => File.Exists(x.Key))) 52 | { 53 | file.Value.Delete(); 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/dotnetCampus.Configurations.WPFTypeConverter/Program.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | using System; 3 | using System.Runtime.CompilerServices; 4 | using System.Windows; 5 | 6 | namespace dotnetCampus.Configurations.WPFTypeConverter 7 | { 8 | /// 9 | /// 提供 Configuration 转换 WPF 的 的方法 10 | /// 11 | public static class SizeConverter 12 | { 13 | /// 14 | /// 设置值 15 | /// 16 | /// 17 | /// 18 | /// 19 | public static void SetValue(this Configuration configuration, Size? value, 20 | [CallerMemberName] string key = "") 21 | { 22 | var sizeText = Serialize(value); 23 | configuration.SetValue(sizeText, key); 24 | } 25 | 26 | /// 27 | /// 获取值 28 | /// 29 | /// 30 | /// 31 | /// 32 | public static Size? GetSize(this Configuration configuration, 33 | [CallerMemberName] string key = "") 34 | { 35 | var sizeText = configuration.GetValue(key); 36 | 37 | try 38 | { 39 | return Deserialize(sizeText); 40 | } 41 | catch (Exception e) 42 | { 43 | throw new ArgumentException($"存储Size的值不合法 key={key} value={sizeText}", e); 44 | } 45 | } 46 | 47 | private static string Serialize(Size? value) 48 | { 49 | return value.HasValue ? $"{value.Value.Width};{value.Value.Height}" : string.Empty; 50 | } 51 | 52 | private static Size? Deserialize(string? sizeText) 53 | { 54 | if (string.IsNullOrEmpty(sizeText)) 55 | { 56 | return null; 57 | } 58 | 59 | string size = sizeText!; 60 | 61 | var n = size.IndexOf(';'); 62 | 63 | var width = size.Substring(0, n); 64 | var height = size.Substring(n + 1, size.Length - n - 1); 65 | 66 | return new Size(double.Parse(width), double.Parse(height)); 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /src/dotnetCampus.Configurations/Threading/CountLimitOperationToken.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | // ReSharper disable once CheckNamespace 4 | namespace dotnetCampus.Threading 5 | { 6 | /// 7 | /// 为次数限制的异步等待操作提供操作。 8 | /// 9 | public class CountLimitOperationToken 10 | { 11 | private readonly long _countLimit; 12 | private long _passed; 13 | 14 | /// 15 | /// 创建一个具有指定执行次数限制的 。 16 | /// 17 | /// 次数限制,可能是不精确的。 18 | public CountLimitOperationToken(long countLimit) 19 | { 20 | Operation = new ContinuousPartOperation(); 21 | _countLimit = countLimit < 0 ? long.MaxValue : countLimit; 22 | } 23 | 24 | /// 25 | /// 获取一个可 await 等待的等待对象。 26 | /// 27 | public ContinuousPartOperation Operation { get; } 28 | 29 | /// 30 | /// 完成此异步操作。 31 | /// 32 | /// 33 | /// 默认情况下,如果此前发生过异常,则认为那是重试过程中的中间异常,现在成功完成了任务,所以中间异常需要移除。 34 | /// 不过,你也可以选择不移除,意味着此任务的完成属于强制终止,而不是成功完成。 35 | /// 36 | public void Complete(bool removeIntermediateExceptions = true) 37 | { 38 | if (removeIntermediateExceptions) 39 | { 40 | Operation.CleanException(); 41 | } 42 | 43 | if (!Operation.IsCompleted) 44 | { 45 | Operation.Complete(); 46 | } 47 | } 48 | 49 | /// 50 | /// 通知此 自上次调用 方法以来增加的次数。 51 | /// 52 | /// 自上次调用 方法以来增加的次数。 53 | public void Pass(long countPassed) 54 | { 55 | _passed += countPassed; 56 | if (_passed >= _countLimit && !Operation.IsCompleted) 57 | { 58 | Operation.Complete(); 59 | } 60 | } 61 | 62 | /// 63 | /// 使用一个新的 来设置此异步操作完成对象。 64 | /// 65 | /// 一个异常,当设置后,同步或异步等待此对象时将抛出异常。 66 | public void UpdateException(Exception exception) 67 | { 68 | if (!Operation.IsCompleted) 69 | { 70 | Operation.UpdateException(exception); 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/dotnetCampus.Configurations/Threading/TimeoutOperationToken.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | // ReSharper disable once CheckNamespace 4 | namespace dotnetCampus.Threading 5 | { 6 | /// 7 | /// 为超时的异步等待操作提供操作。 8 | /// 9 | public class TimeoutOperationToken 10 | { 11 | private readonly TimeSpan _timeout; 12 | private TimeSpan _passed; 13 | 14 | /// 15 | /// 创建一个具有指定超时时间的 。 16 | /// 17 | /// 超时时间,可能不是精确的。 18 | public TimeoutOperationToken(TimeSpan timeout) 19 | { 20 | Operation = new ContinuousPartOperation(); 21 | _timeout = timeout < TimeSpan.Zero ? TimeSpan.MaxValue : timeout; 22 | } 23 | 24 | /// 25 | /// 获取一个可 await 等待的等待对象。 26 | /// 27 | public ContinuousPartOperation Operation { get; } 28 | 29 | /// 30 | /// 完成此异步操作。 31 | /// 32 | /// 33 | /// 默认情况下,如果此前发生过异常,则认为那是重试过程中的中间异常,现在成功完成了任务,所以中间异常需要移除。 34 | /// 不过,你也可以选择不移除,意味着此任务的完成属于强制终止,而不是成功完成。 35 | /// 36 | public void Complete(bool removeIntermediateExceptions = true) 37 | { 38 | if (removeIntermediateExceptions) 39 | { 40 | Operation.CleanException(); 41 | } 42 | 43 | if (!Operation.IsCompleted) 44 | { 45 | Operation.Complete(); 46 | } 47 | } 48 | 49 | /// 50 | /// 通知此 自上次调用 方法以来经过的时间。 51 | /// 52 | /// 自上次调用 方法以来经过的时间。 53 | public void Pass(TimeSpan passedTimeSinceLastPass) 54 | { 55 | _passed += passedTimeSinceLastPass; 56 | if (_passed >= _timeout && !Operation.IsCompleted) 57 | { 58 | Operation.Complete(); 59 | } 60 | } 61 | 62 | /// 63 | /// 使用一个新的 来设置此异步操作完成对象。 64 | /// 65 | /// 一个异常,当设置后,同步或异步等待此对象时将抛出异常。 66 | public void UpdateException(Exception exception) 67 | { 68 | if (!Operation.IsCompleted) 69 | { 70 | Operation.UpdateException(exception); 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /src/dotnetCampus.Configurations/IO/FileSystemWatcherWeakEventRelay.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using dotnetCampus.WeakEvents; 3 | 4 | namespace dotnetCampus.Configurations.IO 5 | { 6 | /// 7 | /// 此类型由机器生成,有关此类型生成的详细信息,请参阅 , 8 | /// 或者阅读文档:https://blog.walterlv.com/post/implement-custom-dotnet-weak-event-relay.html 9 | /// 10 | internal sealed class FileSystemWatcherWeakEventRelay : WeakEventRelay 11 | { 12 | public FileSystemWatcherWeakEventRelay(FileSystemWatcher eventSource) : base(eventSource) { } 13 | 14 | private readonly WeakEvent _created = new WeakEvent(); 15 | private readonly WeakEvent _changed = new WeakEvent(); 16 | private readonly WeakEvent _renamed = new WeakEvent(); 17 | private readonly WeakEvent _deleted = new WeakEvent(); 18 | 19 | public event FileSystemEventHandler Created 20 | { 21 | add => Subscribe(o => o.Created += OnCreated, () => _created.Add(value, value.Invoke)); 22 | remove => _created.Remove(value); 23 | } 24 | 25 | public event FileSystemEventHandler Changed 26 | { 27 | add => Subscribe(o => o.Changed += OnChanged, () => _changed.Add(value, value.Invoke)); 28 | remove => _changed.Remove(value); 29 | } 30 | 31 | public event RenamedEventHandler Renamed 32 | { 33 | add => Subscribe(o => o.Renamed += OnRenamed, () => _renamed.Add(value, value.Invoke)); 34 | remove => _renamed.Remove(value); 35 | } 36 | 37 | public event FileSystemEventHandler Deleted 38 | { 39 | add => Subscribe(o => o.Deleted += OnDeleted, () => _deleted.Add(value, value.Invoke)); 40 | remove => _deleted.Remove(value); 41 | } 42 | 43 | private void OnCreated(object sender, FileSystemEventArgs e) => TryInvoke(_created, sender, e); 44 | private void OnChanged(object sender, FileSystemEventArgs e) => TryInvoke(_changed, sender, e); 45 | private void OnRenamed(object sender, RenamedEventArgs e) => TryInvoke(_renamed, sender, e); 46 | private void OnDeleted(object sender, FileSystemEventArgs e) => TryInvoke(_deleted, sender, e); 47 | 48 | protected override void OnReferenceLost(FileSystemWatcher source) 49 | { 50 | source.Created -= OnCreated; 51 | source.Changed -= OnChanged; 52 | source.Renamed -= OnRenamed; 53 | source.Deleted -= OnDeleted; 54 | source.Dispose(); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/dotnetCampus.Configurations/Core/MemoryConfigurationRepo.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | 5 | namespace dotnetCampus.Configurations.Core 6 | { 7 | /// 8 | /// 使用内存存储的配置文件仓库 9 | /// 10 | public class MemoryConfigurationRepo : AsynchronousConfigurationRepo 11 | { 12 | /// 13 | /// 创建使用内存存储的配置文件仓库 14 | /// 15 | public MemoryConfigurationRepo() 16 | { 17 | _memoryConfiguration = new ConcurrentDictionary(); 18 | } 19 | 20 | /// 21 | /// 创建使用内存存储的配置文件仓库 22 | /// 23 | /// 传入的参数将会被作为初始化的数据 24 | public MemoryConfigurationRepo(IEnumerable> initData) 25 | { 26 | _memoryConfiguration = new ConcurrentDictionary(initData); 27 | } 28 | 29 | /// 30 | /// 创建使用内存存储的配置文件仓库 31 | /// 32 | /// 传入的参数将会用来做存储的对象 33 | public MemoryConfigurationRepo(ConcurrentDictionary memoryStorageDictionary) 34 | { 35 | _memoryConfiguration = memoryStorageDictionary; 36 | } 37 | 38 | /// 39 | /// 获取内存实际采用的存储 40 | /// 41 | /// 42 | public ConcurrentDictionary GetMemoryStorageDictionary() => _memoryConfiguration; 43 | 44 | protected override ICollection GetKeys() 45 | { 46 | return _memoryConfiguration.Keys; 47 | } 48 | 49 | protected override Task ReadValueCoreAsync(string key) 50 | { 51 | if (_memoryConfiguration.TryGetValue(key, out var value)) 52 | { 53 | return Task.FromResult((string?) value); 54 | } 55 | 56 | return Task.FromResult((string?) null); 57 | } 58 | 59 | protected override Task WriteValueCoreAsync(string key, string value) 60 | { 61 | _memoryConfiguration.AddOrUpdate(key, _ => value, (_, __) => value); 62 | 63 | return CompleteTask; 64 | } 65 | 66 | protected override Task RemoveValueCoreAsync(string key) 67 | { 68 | _memoryConfiguration.TryRemove(key, out _); 69 | return CompleteTask; 70 | } 71 | 72 | protected override void OnChanged(AsynchronousConfigurationChangeContext context) 73 | { 74 | // 啥都不需要做 75 | // 不需要持久化,因为这个类是内存配置 76 | } 77 | 78 | private readonly ConcurrentDictionary _memoryConfiguration; 79 | 80 | private Task CompleteTask { get; } 81 | #if NET45 82 | = Task.FromResult(true); 83 | #else 84 | = Task.CompletedTask; 85 | #endif 86 | } 87 | } -------------------------------------------------------------------------------- /src/dotnetCampus.Configurations/Runtime/OperationResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | #pragma warning disable CA2225 // Operator overloads have named alternates 5 | #pragma warning disable CA1815 // Override equals and operator equals on value types 6 | // ReSharper disable once CheckNamespace 7 | 8 | namespace dotnetCampus.Threading 9 | { 10 | /// 11 | /// 为一个操作包装结果信息,包括成功与否、异常和取消信息。 12 | /// 13 | [StructLayout(LayoutKind.Auto)] 14 | public readonly struct OperationResult 15 | { 16 | /// 17 | /// 使用指定的异常创建 的新实例。 18 | /// 这个操作结果是失败的。 19 | /// 20 | /// 操作过程中收集到的异常。 21 | public OperationResult(Exception exception) 22 | { 23 | Exception = exception ?? throw new ArgumentNullException(nameof(exception)); 24 | IsCanceled = false; 25 | } 26 | 27 | /// 28 | /// 创建一个成功的或者取消的 。 29 | /// 30 | /// 31 | /// 如果为 true,则创建一个成功的操作结果;如果为 false,创建一个取消的操作结果。 32 | /// 33 | public OperationResult(bool isSuccessOrCanceled) 34 | { 35 | Exception = null; 36 | IsCanceled = !isSuccessOrCanceled; 37 | } 38 | 39 | /// 40 | /// 获取一个值,该值指示操作已经成功完成。 41 | /// 42 | public bool Success => Exception == null && IsCanceled is false; 43 | 44 | /// 45 | /// 获取操作过程中发生或收集的异常。 46 | /// 47 | public Exception? Exception { get; } 48 | 49 | /// 50 | /// 获取此操作是否已被取消。 51 | /// 52 | public bool IsCanceled { get; } 53 | 54 | /// 55 | /// 将操作结果视为成功与否的 bool 值。 56 | /// 57 | public static implicit operator bool(OperationResult result) => result.Success; 58 | 59 | /// 60 | /// 将操作结果视为异常。 61 | /// 62 | public static implicit operator Exception?(OperationResult result) => result.Exception; 63 | 64 | /// 65 | /// 将异常作为操作结果使用。 66 | /// 67 | public static implicit operator OperationResult(Exception exception) => exception is null 68 | ? new OperationResult(true) 69 | : new OperationResult(exception); 70 | 71 | /// 72 | /// 将成功或取消信息作为操作结果使用。 73 | /// 74 | public static implicit operator OperationResult(bool isSuccessOrCanceled) 75 | => new OperationResult(isSuccessOrCanceled); 76 | 77 | /// 78 | /// 判断操作是否是成功的。 79 | /// 80 | public static bool operator true(OperationResult result) => result.Success; 81 | 82 | /// 83 | /// 判断操作是否是失败的。 84 | /// 85 | public static bool operator false(OperationResult result) => !result.Success; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/dotnetCampus.Configurations/dotnetCampus.Configurations.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0;net45;netcoreapp3.0 5 | latest 6 | enable 7 | $(WarningsAsErrors);CS8600;CS8601;CS8602;CS8603;CS8604;CS8609;CS8610;CS8616;CS8618;CS8619;CS8622;CS8625 8 | dotnetCampus.Configurations 9 | true 10 | walterlv 11 | dotnet-campus 12 | MIT 13 | https://github.com/dotnet-campus/dotnetCampus.Configurations 14 | https://github.com/dotnet-campus/dotnetCampus.Configurations.git 15 | git 16 | true 17 | Debug;Release;UnitTest 18 | COIN 硬币格式的高性能配置文件读写库,COIN = Configuration\n,即“配置+换行符”,因默认使用“\n”作为换行符而得名。COIN 设计了一个高性能的应用程序配置文件,以及实现高性能读写这个配置文件的 .NET 库。特点是高性能读写;在初始化阶段使用全异步处理,避免阻塞主流程;多线程和多进程安全;无异常设计等 19 | 20 | 21 | 22 | $(DefineConstants);UNITTEST 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | true 34 | 35 | true 36 | 37 | 38 | 39 | 40 | 41 | true 42 | 43 | 44 | 45 | true 46 | snupkg 47 | 48 | 49 | true 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /tests/dotnetCampus.Configurations.Tests/Fakes/DebugConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | using dotnetCampus.Configurations.Converters; 4 | 5 | namespace dotnetCampus.Configurations.Tests.Fakes 6 | { 7 | internal sealed class DebugConfiguration : Configuration 8 | { 9 | public bool? IsTested 10 | { 11 | get => GetBoolean(); 12 | set => SetValue(value); 13 | } 14 | 15 | public decimal? Amount 16 | { 17 | get => GetDecimal(); 18 | set => SetValue(value); 19 | } 20 | 21 | public double? OffsetX 22 | { 23 | get => GetDouble(); 24 | set => SetValue(value); 25 | } 26 | 27 | public float? SizeX 28 | { 29 | get => GetSingle(); 30 | set => SetValue(value); 31 | } 32 | 33 | public int? Count 34 | { 35 | get => GetInt32(); 36 | set => SetValue(value); 37 | } 38 | 39 | public int Count2 40 | { 41 | get => GetInt32() ?? 0; 42 | set => SetValue(value); 43 | } 44 | 45 | public long? Length 46 | { 47 | get => GetInt64(); 48 | set => SetValue(value); 49 | } 50 | 51 | public long Length2 52 | { 53 | get => GetInt64() ?? 0L; 54 | set => SetValue(value); 55 | } 56 | 57 | public string Message 58 | { 59 | get => GetString(); 60 | set => SetValue(value); 61 | } 62 | 63 | public string Host 64 | { 65 | get => GetString() ?? "https://localhost:17134"; 66 | set => SetValue(value); 67 | } 68 | 69 | public MethodImplOptions MethodImpl 70 | { 71 | get => this.GetValue() ?? MethodImplOptions.AggressiveInlining; 72 | set => this.SetValue(value); 73 | } 74 | 75 | public MethodImplOptions? MethodImpl2 76 | { 77 | get => this.GetValue(); 78 | set => this.SetValue(value); 79 | } 80 | 81 | public DateTime DateTime 82 | { 83 | get => this.GetValue() ?? default; 84 | set => this.SetValue(value); 85 | } 86 | 87 | public DateTimeOffset? DateTimeOffset 88 | { 89 | get => this.GetValue(); 90 | set => this.SetValue(value); 91 | } 92 | 93 | //public Rect? Bounds 94 | //{ 95 | // get => this.GetValue() ?? new Rect(10, 20, 100, 200); 96 | // set => this.SetValue(Equals(value, new Rect(10, 20, 100, 200)) ? null : value); 97 | //} 98 | 99 | //public Color? Color 100 | //{ 101 | // get => this.GetValue(); 102 | // set => this.SetValue(value); 103 | //} 104 | 105 | public void Clear() 106 | { 107 | ClearValues(); 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/dotnetCampus.Configurations/Concurrent/ProcessConcurrentDictionary.interface.cs: -------------------------------------------------------------------------------- 1 | #nullable disable 2 | using System; 3 | using System.Collections; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | 7 | namespace dotnetCampus.Configurations.Concurrent 8 | { 9 | /// 10 | /// 跨进程并发的字典。 11 | /// 12 | partial class ProcessConcurrentDictionary 13 | { 14 | /// 15 | bool ICollection>.Contains(KeyValuePair item) 16 | => _keyValues.TryGetValue(item.Key, out var value) && Equals(value, item); 17 | 18 | /// 19 | bool IDictionary.TryGetValue(TKey key, out TValue value) 20 | { 21 | if (_keyValues.TryGetValue(key, out var entry)) 22 | { 23 | value = entry.Value; 24 | return true; 25 | } 26 | else 27 | { 28 | value = default; 29 | return false; 30 | } 31 | } 32 | 33 | /// 34 | void IDictionary.Add(TKey key, TValue value) => _keyValues.TryAdd(key, CreateInternalValue(value)); 35 | 36 | /// 37 | void ICollection>.Add(KeyValuePair item) => _keyValues.TryAdd(item.Key, CreateInternalValue(item.Value)); 38 | 39 | /// 40 | bool IDictionary.Remove(TKey key) => _keyValues.TryRemove(key, out _); 41 | 42 | /// 43 | bool ICollection>.Remove(KeyValuePair item) => _keyValues.TryRemove(item.Key, out _); 44 | 45 | /// 46 | void ICollection>.Clear() => _keyValues.Clear(); 47 | 48 | /// 49 | ICollection IDictionary.Keys => _keyValues.Keys; 50 | 51 | /// 52 | ICollection IDictionary.Values => _keyValues.Values.Select(x => x.Value).ToList(); 53 | 54 | /// 55 | int ICollection>.Count => _keyValues.Count; 56 | 57 | /// 58 | bool ICollection>.IsReadOnly => false; 59 | 60 | /// 61 | void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) 62 | { 63 | if (array is null) 64 | { 65 | throw new ArgumentNullException(nameof(array)); 66 | } 67 | 68 | if (arrayIndex < 0) 69 | { 70 | throw new ArgumentOutOfRangeException(nameof(arrayIndex), "序号必须大于等于 0。"); 71 | } 72 | 73 | _keyValues.ToArray().CopyTo(array, arrayIndex); 74 | } 75 | 76 | /// 77 | IEnumerator> IEnumerable>.GetEnumerator() 78 | { 79 | foreach (var pair in _keyValues) 80 | { 81 | yield return new KeyValuePair(pair.Key, pair.Value.Value); 82 | } 83 | } 84 | 85 | /// 86 | IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable>)this).GetEnumerator(); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /tests/dotnetCampus.Configurations.MicrosoftExtensionsConfiguration.Tests/MicrosoftConfigurationExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.Configuration.Memory; 5 | using Microsoft.Extensions.Primitives; 6 | using Microsoft.VisualStudio.TestTools.UnitTesting; 7 | using MSTest.Extensions.Contracts; 8 | using ConfigurationManager = Microsoft.Extensions.Configuration.ConfigurationManager; 9 | 10 | namespace dotnetCampus.Configurations.MicrosoftExtensionsConfiguration.Tests 11 | { 12 | [TestClass] 13 | public class MicrosoftConfigurationExtensions 14 | { 15 | [ContractTestCase] 16 | public void WorkWithJson() 17 | { 18 | "通过读取 Json 配置,不会与 AppConfigurator 冲突".Test(() => 19 | { 20 | // Arrange 21 | var configurationManager = new ConfigurationManager(); 22 | configurationManager.AddJsonFile("appsettings.json"); 23 | 24 | // Assert 25 | var logLevelConfigurationList = configurationManager.GetSection("Logging").GetSection("LogLevel").AsEnumerable(true).ToList(); 26 | Assert.AreEqual(2,logLevelConfigurationList.Count); 27 | 28 | // Act 29 | // 接着对接上 AppConfigurator 之后,还能正常获取到配置内容 30 | var appConfigurator = configurationManager.ToAppConfigurator(); 31 | 32 | Assert.IsNotNull(appConfigurator); 33 | 34 | // Assert 35 | // 期望能获取到和没有对接之前一样的值 36 | logLevelConfigurationList = configurationManager.GetSection("Logging").GetSection("LogLevel").AsEnumerable(true).ToList(); 37 | Assert.AreEqual(2, logLevelConfigurationList.Count); 38 | }); 39 | } 40 | 41 | [ContractTestCase] 42 | public void ConfigurationBuilderToAppConfigurator() 43 | { 44 | "加入配置构建的内容,可以通过 AppConfigurator 设置,设置之后可以读取到".Test(() => 45 | { 46 | // Arrange 47 | const string key = "LindexiIsDoubi"; 48 | const string value = "doubi"; 49 | 50 | IConfigurationBuilder builder = new ConfigurationBuilder(); 51 | 52 | // Act 53 | var appConfigurator = builder.ToAppConfigurator(); 54 | appConfigurator.Default[key] = value; 55 | 56 | var configurationRoot = builder.Build(); 57 | 58 | // Assert 59 | Assert.AreEqual(value, configurationRoot[key]); 60 | }); 61 | } 62 | 63 | [ContractTestCase] 64 | public void ConfigurationToAppConfigurator() 65 | { 66 | "原本在 IConfiguration 存放的内容,可以通过 AppConfigurator 创建出来的配置读取到".Test(() => 67 | { 68 | // Arrange 69 | const string key = "LindexiIsDoubi"; 70 | const string value = "doubi"; 71 | var memoryConfigurationSource = new MemoryConfigurationSource() 72 | { 73 | InitialData = new List>() 74 | { 75 | new KeyValuePair(key, value) 76 | } 77 | }; 78 | IConfigurationProvider configurationProvider=new MemoryConfigurationProvider(memoryConfigurationSource); 79 | IConfiguration configuration = new ConfigurationRoot(new List(){ configurationProvider }); 80 | 81 | // Act 82 | var appConfigurator = configuration.ToAppConfigurator(); 83 | var configurationValue = appConfigurator.Default[key]; 84 | 85 | // Assert 86 | Assert.IsNotNull(configurationValue); 87 | Assert.AreEqual(value, configurationValue.ToString()); 88 | }); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/dotnetCampus.Configurations/ConfigurationString.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Runtime.InteropServices; 4 | 5 | #pragma warning disable CA2225 6 | 7 | namespace dotnetCampus.Configurations 8 | { 9 | /// 10 | /// 表示从 中读取出来的配置项字符串的值。 11 | /// 12 | [StructLayout(LayoutKind.Auto)] 13 | public readonly struct ConfigurationString : IEquatable, IEquatable 14 | { 15 | private readonly string? _value; 16 | 17 | /// 18 | /// 创建 的新实例。 19 | /// 20 | /// 原始字符串的值,允许为 null。 21 | private ConfigurationString(string value) 22 | { 23 | _value = value; 24 | } 25 | 26 | /// 27 | /// 将字符串转换为 以获取可空值类型和非空字符串两者的使用体验优势。 28 | /// 29 | /// 30 | public static implicit operator ConfigurationString?(string? value) 31 | { 32 | return Convert(value); 33 | } 34 | 35 | /// 36 | /// 内部的获取值方法,用于在内部获取值,过程中没有转换和判断,提升一点性能 37 | /// 38 | /// 39 | internal string? GetRawValueInternal() => _value; 40 | 41 | private static ConfigurationString? Convert(string? value) => value == null || string.IsNullOrEmpty(value) ? (ConfigurationString?)null : new ConfigurationString(value); 42 | 43 | /// 44 | /// 调用 方法以便将 转换为非 null 字符串。 45 | /// 46 | /// 中读取出来的配置项字符串的值。 47 | public static implicit operator string(ConfigurationString? configurationValue) 48 | { 49 | return configurationValue?.ToString() ?? string.Empty; 50 | } 51 | 52 | /// 53 | /// 转换为非 null 字符串。如果原始值为 null,将得到 54 | /// 注意: 55 | /// - value.ToString() 可以拿到一定非 null 的字符串; 56 | /// - value?.ToString() 则可以在字符串为 null/"" 时拿到 null。 57 | /// 58 | /// 非 null 字符串。 59 | public override string ToString() 60 | { 61 | return _value is null || string.IsNullOrEmpty(_value) ? string.Empty : _value; 62 | } 63 | 64 | /// 65 | public override bool Equals(object? other) 66 | { 67 | if (other is ConfigurationString cs) 68 | { 69 | return Equals(cs); 70 | } 71 | 72 | if (other is string s) 73 | { 74 | return Equals(s); 75 | } 76 | 77 | return false; 78 | } 79 | 80 | /// 81 | public bool Equals(ConfigurationString other) => _value == other._value; 82 | 83 | /// 84 | public bool Equals(string other) => _value == other; 85 | 86 | /// 87 | public override int GetHashCode() => string.IsNullOrEmpty(_value) ? 0 : StringComparer.Ordinal.GetHashCode(_value); 88 | 89 | /// 90 | /// 判断两个 的字符串值是否相等。 91 | /// 注意,空字符串和 null 会被视为相等,因为这两者在 中表示的是相同含义。 92 | /// 93 | public static bool operator ==(ConfigurationString left, ConfigurationString right) => left.Equals(right); 94 | 95 | /// 96 | /// 判断两个 的字符串值是否不相等。 97 | /// 注意,空字符串和 null 会被视为相等,因为这两者在 中表示的是相同含义。 98 | /// 99 | public static bool operator !=(ConfigurationString left, ConfigurationString right) => !(left == right); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/dotnetCampus.Configurations/DefaultConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.IO; 4 | using dotnetCampus.Configurations.Core; 5 | 6 | namespace dotnetCampus.Configurations 7 | { 8 | /// 9 | /// 为所有的应用程序配置项提供与 的交互。 10 | /// 派生类继承此基类时,添加属性以存储配置。 11 | /// 12 | public sealed class DefaultConfiguration : Configuration 13 | { 14 | /// 15 | /// 创建用于给 管理配置的默认配置。 16 | /// 17 | public DefaultConfiguration() : base(null) 18 | { 19 | } 20 | 21 | /// 22 | /// 获取用标识符描述的配置项的字符串值。 23 | /// 在获取值时,会得到可当作字符串使用的值,你可以直接将其赋值给需要字符串类型的属性上,而且你永远不会得到值为 null 的字符串。 24 | /// 另外,由于这是一个可空结构体,所以你也可以直接通过 is null 或者 == null 来判断此值是否没有被设置过,或者使用 ?? 空传递运算符来指定默认值。 25 | /// 在设置值时,你可以直接将一个字符串或者 null 设置进来而不会出现异常。 26 | /// 27 | /// 配置项的标识符。 28 | /// 配置项的值。 29 | public ConfigurationString? this[string key] 30 | { 31 | get => GetString(key); 32 | set => SetValue(value, key); 33 | } 34 | 35 | /// 36 | /// 管理不同文件的 的实例。 37 | /// 38 | private static readonly ConcurrentDictionary> Configurations 39 | = new ConcurrentDictionary>(StringComparer.OrdinalIgnoreCase); 40 | 41 | /// 42 | /// 从文件创建默认的配置管理器,你将可以使用类似字典的方式管理线程和进程安全的应用程序配置。 43 | /// 对于同一个文件,此方法会获取到相同的 的实例。 44 | /// 此方法是线程安全的。 45 | /// 46 | /// 来自于本地文件系统的文件名/路径。文件或文件所在的文件夹不需要提前存在。 47 | /// 一个默认的应用程序配置。 48 | public static DefaultConfiguration FromFile(string fileName) 49 | { 50 | if (fileName == null) 51 | { 52 | throw new ArgumentNullException(nameof(fileName)); 53 | } 54 | 55 | if (string.IsNullOrWhiteSpace(fileName)) 56 | { 57 | throw new ArgumentException("文件名不能使用空字符串。", nameof(fileName)); 58 | } 59 | 60 | var path = Path.GetFullPath(fileName); 61 | var reference = Configurations.GetOrAdd(path, CreateConfigurationReference); 62 | 63 | // 以下两个 if 一个 lock 是类似于单例模式的创建方式,既保证性能又保证只创建一次。 64 | if (!reference.TryGetTarget(out var config)) 65 | { 66 | lock (reference) 67 | { 68 | if (!reference.TryGetTarget(out config)) 69 | { 70 | config = CreateConfiguration(path); 71 | reference.SetTarget(config); 72 | } 73 | } 74 | } 75 | 76 | return config; 77 | } 78 | 79 | /// 80 | /// 创建 的弱引用实例。 81 | /// 为了保证线程安全,此方法仅能被 访问。 82 | /// 83 | /// 已经过验证的完整文件路径。 84 | /// 的弱引用实例。 85 | private static WeakReference CreateConfigurationReference(string path) 86 | => new WeakReference( 87 | CreateConfiguration(path)); 88 | 89 | /// 90 | /// 创建 的新实例。 91 | /// 为了保证线程安全,此方法仅能被 访问。 92 | /// 93 | /// 已经过验证的完整文件路径。 94 | /// 的新实例。 95 | private static DefaultConfiguration CreateConfiguration(string path) 96 | => ConfigurationFactory.FromFile(path).CreateAppConfigurator().Of(); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/dotnetCampus.Configurations/Threading/ContinuousPartOperation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Runtime.CompilerServices; 4 | using System.Threading.Tasks; 5 | 6 | #pragma warning disable CA1034 7 | 8 | // ReSharper disable once CheckNamespace 9 | namespace dotnetCampus.Threading 10 | { 11 | /// 12 | /// 为一个持续操作中的一部分提供可异步等待的操作。 13 | /// 14 | public class ContinuousPartOperation 15 | { 16 | private readonly TaskCompletionSource _source; 17 | private readonly Awaiter _awaiter; 18 | private Action? _continuation; 19 | private Exception? _exception; 20 | 21 | internal ContinuousPartOperation() 22 | { 23 | _source = new TaskCompletionSource(); 24 | _awaiter = new Awaiter(this); 25 | } 26 | 27 | /// 28 | /// 获取一个值,该值指示此异步操作是否已经结束。 29 | /// 30 | internal bool IsCompleted { get; private set; } 31 | 32 | /// 33 | /// 完成此异步操作。 34 | /// 35 | internal void Complete() 36 | { 37 | IsCompleted = true; 38 | if (_exception == null) 39 | { 40 | _source.SetResult(null); 41 | } 42 | else 43 | { 44 | _source.SetException(_exception); 45 | } 46 | 47 | var continuation = _continuation; 48 | _continuation = null; 49 | continuation?.Invoke(); 50 | } 51 | 52 | /// 53 | /// 使用一个新的 来设置此异步操作完成对象。 54 | /// 55 | /// 一个异常,当设置后,同步或异步等待此对象时将抛出异常。 56 | internal void UpdateException(Exception exception) 57 | => _exception = exception ?? throw new ArgumentNullException(nameof(exception)); 58 | 59 | /// 60 | /// 清除此异步等待操作中使用 设置过的异常。 61 | /// 这样,异步等待的类型在等待结束时不会引发一个异常。 62 | /// 63 | internal void CleanException() => _exception = null; 64 | 65 | /// 66 | /// 获取一个用于等待此异步操作的可等待对象。 67 | /// 68 | public Awaiter GetAwaiter() => _awaiter; 69 | 70 | /// 71 | /// 同步等待此异步操作完成。 72 | /// 73 | public void Wait() => _source.Task.GetAwaiter().GetResult(); 74 | 75 | /// 76 | /// 表示用于等待 的异步可等待对像。 77 | /// 78 | public sealed class Awaiter : INotifyCompletion 79 | { 80 | private readonly ContinuousPartOperation _owner; 81 | 82 | /// 83 | /// 创建一个用于等待 的异步可等待对象。 84 | /// 85 | internal Awaiter(ContinuousPartOperation owner) 86 | { 87 | _owner = owner; 88 | } 89 | 90 | /// Schedules the continuation action that's invoked when the instance completes. 91 | /// The action to invoke when the operation completes. 92 | /// The continuation argument is null (Nothing in Visual Basic). 93 | public void OnCompleted(Action continuation) 94 | { 95 | if (IsCompleted) 96 | { 97 | continuation?.Invoke(); 98 | } 99 | else 100 | { 101 | _owner._continuation += continuation; 102 | } 103 | } 104 | 105 | /// 106 | /// 获取一个值,该值指示异步操作是否完成。 107 | /// 108 | public bool IsCompleted => _owner.IsCompleted; 109 | 110 | /// 111 | /// 获取此异步操作的结果。 112 | /// 113 | [DebuggerStepThrough] 114 | public void GetResult() => _owner._source.Task.GetAwaiter().GetResult(); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/dotnetCampus.Configurations.MicrosoftExtensionsConfiguration/MicrosoftExtensionsConfigurationBuildRepo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using dotnetCampus.Configurations.Core; 5 | using Microsoft.Extensions.Configuration; 6 | using Microsoft.Extensions.Primitives; 7 | 8 | namespace dotnetCampus.Configurations.MicrosoftExtensionsConfiguration 9 | { 10 | internal class MicrosoftExtensionsConfigurationBuildRepo : ConfigurationRepo 11 | { 12 | public MicrosoftExtensionsConfigurationBuildRepo(IConfigurationBuilder configuration) 13 | { 14 | configuration.Add(new ConfigurationSource(_concurrentDictionary)); 15 | } 16 | 17 | public override string? GetValue(string key) 18 | { 19 | if (_concurrentDictionary.TryGetValue(key, out var value)) 20 | { 21 | return value; 22 | } 23 | 24 | return null; 25 | } 26 | 27 | public override void SetValue(string key, string? value) 28 | { 29 | if (value is null) 30 | { 31 | _concurrentDictionary.TryRemove(key, out _); 32 | } 33 | else 34 | { 35 | _concurrentDictionary[key] = value; 36 | } 37 | } 38 | 39 | public override void ClearValues(Predicate keyFilter) 40 | { 41 | foreach (var key in _concurrentDictionary.Keys) 42 | { 43 | if (keyFilter(key)) 44 | { 45 | _concurrentDictionary.TryRemove(key, out _); 46 | } 47 | } 48 | } 49 | 50 | private readonly ConcurrentDictionary _concurrentDictionary = 51 | new ConcurrentDictionary(); 52 | 53 | class ConfigurationSource : IConfigurationSource 54 | { 55 | public ConfigurationSource(ConcurrentDictionary concurrentDictionary) 56 | { 57 | _concurrentDictionary = concurrentDictionary; 58 | } 59 | 60 | private readonly ConcurrentDictionary _concurrentDictionary; 61 | 62 | public IConfigurationProvider Build(IConfigurationBuilder builder) 63 | { 64 | return new MemoryConfigurationProvider(_concurrentDictionary); 65 | } 66 | } 67 | 68 | class MemoryConfigurationProvider : IConfigurationProvider 69 | { 70 | public MemoryConfigurationProvider(ConcurrentDictionary? concurrentDictionary = null) 71 | { 72 | _concurrentDictionary = concurrentDictionary ?? new ConcurrentDictionary(); 73 | } 74 | 75 | private readonly ConcurrentDictionary _concurrentDictionary; 76 | 77 | public bool TryGet(string key, out string value) 78 | { 79 | return _concurrentDictionary.TryGetValue(key, out value!); 80 | } 81 | 82 | public void Set(string key, string value) 83 | { 84 | _concurrentDictionary[key] = value; 85 | } 86 | 87 | public IChangeToken GetReloadToken() 88 | { 89 | return new ChangeToken(); 90 | } 91 | 92 | class ChangeToken : IChangeToken 93 | { 94 | public IDisposable RegisterChangeCallback(Action callback, object state) => 95 | new EmptyDisposable(); 96 | 97 | public bool HasChanged => false; 98 | public bool ActiveChangeCallbacks => false; 99 | 100 | class EmptyDisposable : IDisposable 101 | { 102 | public void Dispose() 103 | { 104 | } 105 | } 106 | } 107 | 108 | public void Load() 109 | { 110 | } 111 | 112 | public IEnumerable GetChildKeys(IEnumerable earlierKeys, string parentPath) 113 | { 114 | // 这里如果返回空集合,将会清空原有的配置内容。这个函数的作用是用来进行过滤和追加两个合一起,底层框架这样设计是为了性能考虑。在一个配置管理里面,是由多个 IConfigurationProvider 组成,而多个 IConfigurationProvider 之间,需要有相互影响。在获取所有的 GetChildKeys 时候,假定每个 IConfigurationProvider 都需要追加自身的,那传入 IEnumerable 类型,用于追加是最省资源的。而有些 IConfigurationProvider 之间提供了相同的 Key 的配置,但是有些 IConfigurationProvider 期望覆盖,有些期望不覆盖,于是就通过 earlierKeys 即可用来实现过滤判断,每个不同的 IConfigurationProvider 可以有自己的策略,对先加入的 IConfigurationProvider 返回的 GetChildKeys 进行处理 115 | // return Array.Empty(); 116 | return earlierKeys; 117 | } 118 | } 119 | } 120 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # COIN 硬币配置文件 2 | 3 | COIN = Configuration\n,即“配置+换行符”,因默认使用“\n”作为换行符而得名。COIN 设计了一个高性能的应用程序配置文件,以及实现高性能读写这个配置文件的 .NET 库。 4 | 5 | 原名为:dotnetCampus.Configurations,这也是此库中命名空间的前缀。 6 | 7 | |Build|NuGet| 8 | |--|--| 9 | |![](https://github.com/dotnet-campus/dotnetCampus.Configurations/workflows/.NET%20Core/badge.svg)|[![](https://img.shields.io/nuget/v/dotnetCampus.Configurations.svg)](https://www.nuget.org/packages/dotnetCampus.Configurations)| 10 | 11 | ## 配置文件存储格式 12 | 13 | 配置文件以行为单位,将行首是 `>` 字符的行作为注释,在 `>` 后面的内容将会被忽略。在第一个非 `>` 字符开头的行作为 `Key` 值,在此行以下直到文件结束或下一个 `>` 字符开始的行之间内容作为 `Value` 值 14 | 15 | ``` 16 | > 配置文件 17 | > 版本 1.0 18 | State.BuildLogFile 19 | xxxxx 20 | > 注释内容 21 | Foo 22 | 这是第一行 23 | 这是第二行 24 | > 25 | > 配置文件结束 26 | ``` 27 | 28 | ## NuGet 安装 29 | 30 | ```csharp 31 | dotnet add package dotnetCampus.Configurations 32 | ``` 33 | 34 | ## 快速使用 35 | 36 | 初始化: 37 | 38 | ```csharp 39 | // 使用一个文件路径创建默认配置的实例。文件可以存在也可以不存在,甚至其所在的文件夹也可以不需要提前存在。 40 | // 这里的配置文件后缀名 coin 是 Configuration\n,即 “配置+换行符” 的简称。你也可以使用其他扩展名,因为它实际上只是 UTF-8 编码的纯文本而已。 41 | var configs = DefaultConfiguration.FromFile(@"C:\Users\lvyi\Desktop\walterlv.coin"); 42 | ``` 43 | 44 | 获取值: 45 | 46 | ```csharp 47 | // 获取配置 Foo 的字符串值。 48 | // 这里的 value 一定不会为 null,如果文件不存在或者没有对应的配置项,那么为空字符串。 49 | string value0 = configs["Foo"]; 50 | 51 | // 获取字符串值的时候,如果文件不存在或者没有对应的配置项,那么会使用默认值(空传递运算符 ?? 可以用来指定默认值)。 52 | string value1 = configs["Foo"] ?? "anonymous"; 53 | ``` 54 | 55 | 设置值: 56 | 57 | ```csharp 58 | // 设置配置 Foo 的字符串值。 59 | configs["Foo"] = "lvyi"; 60 | 61 | // 可以设置为 null,但你下次再次获取值的时候却依然保证不会返回 null 字符串。 62 | configs["Foo"] = null; 63 | 64 | // 可以设置为空字符串,效果与设置为 null 是等同的。 65 | configs["Foo"] = ""; 66 | ``` 67 | 68 | ## 在大型项目中使用 69 | 70 | 大型项目的模块数量众多,其配置的数量也是十分庞大的。为了保证配置在业务之间独立,也为了防止类型转换辅助代码在大型项目中重复编写,你需要使用更高级的初始化和使用方法。 71 | 72 | 初始化: 73 | 74 | ```csharp 75 | // 这里是大型项目配置初始化处的代码。 76 | // 此类型中包含底层的配置读写方法,而且所有读写全部是异步的,防止影响启动性能。 77 | var configFileName = @"C:\Users\lvyi\Desktop\walterlv.coin"; 78 | var config = ConfigurationFactory.FromFile(configFileName); 79 | 80 | // 如果你需要对整个应用程序公开配置,那么可以公开 CreateAppConfigurator 方法返回的新实例。 81 | // 这个实例的所有配置读写全部是同步方法,这是为了方便其他模块使用。 82 | Container.Set(config.CreateAppConfigurator()); 83 | ``` 84 | 85 | 在业务模块中定义类型安全的配置类: 86 | 87 | ```csharp 88 | internal class StateConfiguration : Configuration 89 | { 90 | /// 91 | /// 获取或设置整型。 92 | /// 93 | internal int? Count 94 | { 95 | get => GetInt32(); 96 | set => SetValue(value); 97 | } 98 | 99 | /// 100 | /// 获取或设置带默认值的整型。 101 | /// 102 | internal int Length 103 | { 104 | get => GetInt32() ?? 2; 105 | set => SetValue(Equals(value, 2) ? null : value); 106 | } 107 | 108 | /// 109 | /// 获取或设置布尔值。 110 | /// 111 | internal bool? State 112 | { 113 | get => GetBoolean(); 114 | set => SetValue(value); 115 | } 116 | 117 | /// 118 | /// 获取或设置字符串。 119 | /// 120 | internal string Value 121 | { 122 | get => GetString(); 123 | set => SetValue(value); 124 | } 125 | 126 | /// 127 | /// 获取或设置带默认值的字符串。 128 | /// 129 | internal string Host 130 | { 131 | get => GetString() ?? "https://localhost:17134"; 132 | set => SetValue(Equals(value, "https://localhost:17134") ? null : value); 133 | } 134 | 135 | /// 136 | /// 获取或设置非基元值类型。 137 | /// 138 | internal Rect? Screen 139 | { 140 | get => this.GetValue(); 141 | set => this.SetValue(value); 142 | } 143 | } 144 | ``` 145 | 146 | 在业务模块中使用: 147 | 148 | ```csharp 149 | private readonly IAppConfiguration _config = Container.Get(); 150 | 151 | // 读取配置。 152 | private void Restore() 153 | { 154 | var config = _config.Of(); 155 | var bounds = config.Screen; 156 | if (bounds != null) 157 | { 158 | // 恢复窗口位置和尺寸。 159 | } 160 | } 161 | 162 | // 写入配置。 163 | public void Update() 164 | { 165 | var config = _config.Of(); 166 | config.Screen = new Rect(0, 0, 3840, 2160); 167 | } 168 | ``` 169 | 170 | ## 特性 171 | 172 | 1. 高性能读写 173 | - 在初始化阶段使用全异步处理,避免阻塞主流程。 174 | - 使用特别为高性能读写而设计的配置文件格式。 175 | - 多线程和多进程安全高性能读写。 176 | 1. 无异常设计 177 | - 所有配置项的读写均为“无异常设计”,你完全不需要在业务代码中处理任何异常。 178 | - 为防止业务代码中出现意料之外的 `NullReferenceException`,所有配置项的返回值均不为实际意义的 `null`。 179 | - 值类型会返回其对应的 `Nullable` 类型,这是一个结构体,虽然有 `null` 值,但不会产生空引用。 180 | - 引用类型仅提供字符串,返回 `Nullable` 类型,这也是一个结构体,你可以判断 `null`,但实际上不可能为 `null`。 181 | 1. 全应用程序统一的 API 182 | - 在大型应用中开放 API 时记得使用 `CreateAppConfigurator()` 来开放,这会让整个应用程序使用统一的一套配置读写 API,且完全的 IO 无感知。 183 | -------------------------------------------------------------------------------- /src/dotnetCampus.Configurations/Utils/WeakEventRelay.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Runtime.CompilerServices; 4 | 5 | namespace dotnetCampus.WeakEvents 6 | { 7 | /// 8 | /// 为已有对象的事件添加弱事件中继,这可以避免町事件的对象因为无法释放而导致的内存泄漏问题。 9 | /// 通过编写一个继承自此类型的自定义类型,可以将原有任何 CLR 事件转换为弱事件。 10 | /// 11 | /// 事件原始引发源的类型。 12 | /// 13 | /// 此弱事件中继要求具有较高的事件中转性能,所以没有使用到任何反射或其他动态调用方法。 14 | /// 为此,编写一个自定义的弱事件中继可能会有些困难,如果需要,请参阅文档: 15 | /// https://blog.walterlv.com/post/implement-custom-dotnet-weak-event-relay.html 16 | /// 17 | internal abstract class WeakEventRelay where TEventSource : class 18 | { 19 | /// 20 | /// 获取事件引发源(也就是事件参数里的那个 sender 参数)。 21 | /// 由于此弱事件中继会在有事件订阅的时候被 sender 强引用,所以两者的生命周期近乎相同,不需要弱引用此对象。 22 | /// 23 | private readonly TEventSource _eventSource; 24 | 25 | /// 26 | /// 保留所有已订阅的事件名(相当于一个线程安全的哈希表)。 27 | /// 这样,每一个原始事件仅仅会真实地订阅一次,专门用于让中转方法被调用一次;当然,最终中转引发弱事件的时候可以有很多次,但与此字段无关。 28 | /// 29 | private readonly ConcurrentDictionary _events = new ConcurrentDictionary(); 30 | 31 | /// 32 | /// 初始化弱事件中继对象的基类属性。 33 | /// 在初始化此实例后,请不要用任何方式保留此实例的引用,除非你自己能处理好事件的注销(-=)。 34 | /// 35 | /// 事件引发源的实例。 36 | protected WeakEventRelay(TEventSource eventSource) => _eventSource = eventSource ?? throw new ArgumentNullException(nameof(eventSource)); 37 | 38 | /// 39 | /// 在派生类中实现自定义事件的中继的时候,需要在事件的 add 方法中调用此方法以订阅弱事件。 40 | /// 41 | /// 请始终写为 o => o.事件名 += On事件名;例如 o => o.Changed += OnChanged。 42 | /// 请始终写为 () => 弱事件.Add(value, value.Invoke);例如 () => _changed.Add(value, value.Invoke)。 43 | /// 请让编译器自动传入此参数。此事件名不会作反射或其他耗性能的用途,仅仅用于防止事件重复订阅造成的额外性能问题。 44 | /// 45 | /// 有关详细写法,请参阅文档: 46 | /// https://blog.walterlv.com/post/implement-custom-dotnet-weak-event-relay.html 47 | /// 48 | protected void Subscribe(Action sourceEventAdder, Action relayEventAdder, [CallerMemberName] string? eventName = null) 49 | { 50 | if (eventName is null) 51 | { 52 | throw new ArgumentNullException(nameof(eventName)); 53 | } 54 | 55 | // <--订阅-- [最终订阅者 1] 56 | // [事件源] <--订阅-- [事件中继] <--订阅-- [最终订阅者 2] 57 | // <--订阅-- [最终订阅者 3] 58 | 59 | if (_events.TryAdd(eventName, eventName)) 60 | { 61 | // 中继仅仅向源事件订阅一次。 62 | sourceEventAdder(_eventSource); 63 | } 64 | 65 | // 但是允许弱事件订阅者订阅很多次弱事件。 66 | relayEventAdder(); 67 | } 68 | 69 | /// 70 | /// 请在原始事件的事件处理函数中调用此方法,并且请始终写为 TryInvoke(弱事件, sender, e)。 71 | /// 72 | /// 事件引发源的类型,可隐式推断。 73 | /// 事件参数的类型,可隐式推断。 74 | /// 弱事件对象,请使用 来创建并存为字段。 75 | /// 源事件的引发者,即 sender。请始终传入 sender。 76 | /// 源事件的事件参数,即 e。请始终传入 e。 77 | /// 78 | /// 有关详细写法,请参阅文档: 79 | /// https://blog.walterlv.com/post/implement-custom-weak-event-relay.html 80 | /// 81 | protected void TryInvoke(WeakEvent weakEvent, TSender sender, TArgs e) 82 | { 83 | // 引发弱事件,并确认是否仍有订阅者存活(未被 GC 回收)。 84 | var anyAlive = weakEvent.Invoke(sender, e); 85 | if (!anyAlive) 86 | { 87 | // 如果没有任何订阅者存活,那么要求派生类清除事件源的订阅,这可以清除此事件中继的实例。 88 | OnReferenceLost(_eventSource); 89 | } 90 | } 91 | 92 | /// 93 | /// 当没有任何事件订阅者存活的时候,会调用此方法。 94 | /// 在派生类中实现此方法的时候,需要清除所有对事件源中全部事件的订阅,以便清除此事件中继的实例。 95 | /// 另外,如果事件源实现了 接口,建议在可能的情况下调用 方法,这可以释放事件源的资源。 96 | /// 97 | /// 事件源的实例。 98 | /// 99 | /// 此方法可能调用多次,也可能永远不会被调用。 100 | /// 如果调用多次,说明事件在引发/回收之后有对象发生了新的订阅。 101 | /// 如果永远不会调用,这是个好事,说明事件源自己已经被回收了,那么此中继对象自然也被回收;这时不调用此方法也不会产生任何泄漏。 102 | /// 103 | protected abstract void OnReferenceLost(TEventSource source); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/dotnetCampus.Configurations/Threading/PartialAwaitableRetry.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | // ReSharper disable once CheckNamespace 7 | namespace dotnetCampus.Threading 8 | { 9 | /// 10 | /// 对于异步的,出错后会重试的操作,使用此类型可以辅助等待循环重试的一部分。 11 | /// 12 | public class PartialAwaitableRetry 13 | { 14 | private readonly object _locker = new object(); 15 | private readonly Func> _loopItem; 16 | private readonly List _tokens = new List(); 17 | private volatile bool _isLooping; 18 | 19 | /// 20 | /// 使用一个循环任务初始化 的一个新实例。 21 | /// 22 | /// 一个循环任务。 23 | public PartialAwaitableRetry(Func> loopItem) 24 | { 25 | _loopItem = loopItem ?? throw new ArgumentNullException(nameof(loopItem)); 26 | } 27 | 28 | /// 29 | /// 以指定的次数限制加入循环,并返回等待此循环结果的可等待对象。 30 | /// 此方法是线程安全的。 31 | /// 32 | /// 次数限制,当设置为 -1 时表示无限次循环。 33 | /// 等待循环结果的可等待对象。 34 | public ContinuousPartOperation JoinAsync(int countLimit) 35 | { 36 | var token = new CountLimitOperationToken(countLimit); 37 | 38 | lock (_locker) 39 | { 40 | _tokens.Add(token); 41 | if (!_isLooping) 42 | { 43 | Loop(); 44 | } 45 | } 46 | 47 | return token.Operation; 48 | } 49 | 50 | /// 51 | /// 执行实际的循环,并在每一次执行的时候会给所有的等待对象报告结果。 52 | /// 53 | private async Task Loop() 54 | { 55 | _isLooping = true; 56 | 57 | var context = new PartialRetryContext(); 58 | var shouldContinue = true; 59 | 60 | try 61 | { 62 | while (shouldContinue) 63 | { 64 | Exception? exception; 65 | bool isCompleted; 66 | 67 | // 加锁获取此时此刻的 Token 集合副本。 68 | // 执行一次循环的时候,只能操作此集合副本,真实集合新增的元素由于没有参与循环操作的执行; 69 | // 这意味着期望执行一次方法的时候却没有执行,所以不能提供结果。 70 | List snapshot; 71 | lock (_locker) 72 | { 73 | snapshot = _tokens.ToList(); 74 | } 75 | 76 | try 77 | { 78 | var result = await _loopItem.Invoke(context).ConfigureAwait(false); 79 | exception = result.Exception; 80 | isCompleted = result.Success; 81 | } 82 | #pragma warning disable CA1031 // Do not catch general exception types 83 | catch (Exception ex) 84 | #pragma warning restore CA1031 // Do not catch general exception types 85 | { 86 | exception = ex; 87 | isCompleted = false; 88 | } 89 | 90 | if (exception != null) 91 | { 92 | foreach (var token in snapshot) 93 | { 94 | token.UpdateException(exception); 95 | } 96 | } 97 | 98 | if (isCompleted) 99 | { 100 | foreach (var token in snapshot) 101 | { 102 | token.Complete(); 103 | } 104 | 105 | lock (_locker) 106 | { 107 | _tokens.RemoveAll(token => snapshot.Contains(token)); 108 | shouldContinue = _tokens.Count > 0; 109 | } 110 | } 111 | else 112 | { 113 | foreach (var token in snapshot) 114 | { 115 | token.Pass(context.StepCount); 116 | } 117 | } 118 | } 119 | } 120 | finally 121 | { 122 | _isLooping = false; 123 | } 124 | } 125 | } 126 | 127 | /// 128 | /// 为 提供循环执行的上下文设置信息。 129 | /// 130 | public sealed class PartialRetryContext 131 | { 132 | private int _stepCount = 1; 133 | 134 | /// 135 | /// 获取或设置此方法一次执行时经过了多少次循环。 136 | /// 当某个方法执行时需要进行不打断的多次循环才能完成时,可以修改此值。 137 | /// 138 | public int StepCount 139 | { 140 | get => _stepCount; 141 | set 142 | { 143 | if (value <= 0) 144 | { 145 | throw new ArgumentException("次数必须大于或等于 1。", nameof(value)); 146 | } 147 | 148 | _stepCount = value; 149 | } 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/dotnetCampus.Configurations/Converters/ConfigurationStringExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | 4 | namespace dotnetCampus.Configurations.Converters; 5 | 6 | /// 7 | /// 对 的扩展方法 8 | /// 9 | public static class ConfigurationStringExtensions 10 | { 11 | /// 12 | /// 将 转换为 ? 类型。如传入 为非法 或空,则都返回 null 值 13 | /// 14 | /// 15 | /// 16 | public static bool? AsBoolean(this ConfigurationString? configurationString) 17 | { 18 | if (configurationString is null) 19 | { 20 | return null; 21 | } 22 | var value = configurationString.Value.GetRawValueInternal(); 23 | return bool.TryParse(value, out var result) ? result : null; 24 | } 25 | 26 | /// 27 | /// 将 转换为 ? 类型。如传入 为非法 或空,则都返回 null 值 28 | /// 29 | /// 30 | /// 31 | public static decimal? AsDecimal(this ConfigurationString? configurationString) 32 | { 33 | if (configurationString is null) 34 | { 35 | return null; 36 | } 37 | 38 | var value = configurationString.Value.GetRawValueInternal(); 39 | return decimal.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result) ? result : null; 40 | } 41 | 42 | /// 43 | /// 将 转换为 ? 类型。如传入 为非法 或空,则都返回 null 值 44 | /// 45 | /// 46 | /// 47 | public static double? AsDouble(this ConfigurationString? configurationString) 48 | { 49 | if (configurationString is null) 50 | { 51 | return null; 52 | } 53 | var value = configurationString.Value.GetRawValueInternal(); 54 | return double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result) ? result : null; 55 | } 56 | 57 | /// 58 | /// 将 转换为 ? 类型。如传入 为非法 或空,则都返回 null 值 59 | /// 60 | /// 61 | /// 62 | public static float? AsSingle(this ConfigurationString? configurationString) 63 | { 64 | if (configurationString is null) 65 | { 66 | return null; 67 | } 68 | var value = configurationString.Value.GetRawValueInternal(); 69 | return float.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result) ? result : null; 70 | } 71 | 72 | /// 73 | [Obsolete("此方法的作用只是用来告诉你,应该调用的是 AsSingle 方法")] 74 | public static float? AsFloat(this ConfigurationString? configurationString) 75 | { 76 | return configurationString.AsSingle(); 77 | } 78 | 79 | /// 80 | /// 将 转换为 ? 类型。如传入 为非法 或空,则都返回 null 值 81 | /// 82 | /// 83 | /// 84 | public static int? AsInt32(this ConfigurationString? configurationString) 85 | { 86 | if (configurationString is null) 87 | { 88 | return null; 89 | } 90 | var value = configurationString.Value.GetRawValueInternal(); 91 | return int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result) ? result : null; 92 | } 93 | 94 | /// 95 | /// 将 转换为 ? 类型。如传入 为非法 或空,则都返回 null 值 96 | /// 97 | /// 98 | /// 99 | public static long? AsInt64(this ConfigurationString? configurationString) 100 | { 101 | if (configurationString is null) 102 | { 103 | return null; 104 | } 105 | var value = configurationString.Value.GetRawValueInternal(); 106 | return long.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result) ? result : null; 107 | } 108 | 109 | /// 110 | /// 将 转换为 ? 类型。如传入 为非法 或空,则都返回 null 值 111 | /// 112 | /// 枚举类型 113 | /// 114 | /// 115 | public static T? AsEnum(this ConfigurationString? configurationString) where T : struct, IConvertible 116 | { 117 | if (configurationString is null) 118 | { 119 | return null; 120 | } 121 | var value = configurationString.Value.GetRawValueInternal(); 122 | 123 | return Enum.TryParse(value, out var result) ? result : null; 124 | } 125 | } -------------------------------------------------------------------------------- /dotnetCampus.Configurations.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29503.13 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{40BD8E4E-62CC-4055-8547-96D733254C87}" 7 | ProjectSection(SolutionItems) = preProject 8 | .gitattributes = .gitattributes 9 | .gitignore = .gitignore 10 | Directory.Build.props = Directory.Build.props 11 | LICENSE = LICENSE 12 | README.md = README.md 13 | build\Version.props = build\Version.props 14 | EndProjectSection 15 | EndProject 16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnetCampus.Configurations", "src\dotnetCampus.Configurations\dotnetCampus.Configurations.csproj", "{CD0B4E3B-7424-4EF0-A339-F290A658EDC6}" 17 | EndProject 18 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnetCampus.Configurations.Tests", "tests\dotnetCampus.Configurations.Tests\dotnetCampus.Configurations.Tests.csproj", "{FDB83C93-ED9F-4B31-A172-D0D0FC584FCD}" 19 | EndProject 20 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnetCampus.Configurations.Benchmark", "tests\dotnetCampus.Configurations.Benchmark\dotnetCampus.Configurations.Benchmark.csproj", "{5739DAB0-143E-4118-8660-98B9DD10971B}" 21 | EndProject 22 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnetCampus.Configurations.WPFTypeConverter", "src\dotnetCampus.Configurations.WPFTypeConverter\dotnetCampus.Configurations.WPFTypeConverter.csproj", "{2E71DCD7-E420-43A5-85A7-01502B8BDAE0}" 23 | EndProject 24 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnetCampus.Configurations.MicrosoftExtensionsConfiguration", "src\dotnetCampus.Configurations.MicrosoftExtensionsConfiguration\dotnetCampus.Configurations.MicrosoftExtensionsConfiguration.csproj", "{3A90D5D0-AA31-48B2-82BD-50A936C90D6C}" 25 | EndProject 26 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnetCampus.Configurations.MicrosoftExtensionsConfiguration.Tests", "tests\dotnetCampus.Configurations.MicrosoftExtensionsConfiguration.Tests\dotnetCampus.Configurations.MicrosoftExtensionsConfiguration.Tests.csproj", "{4AB10B28-5DC6-42FE-8966-5CBC4EB19845}" 27 | EndProject 28 | Global 29 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 30 | Debug|Any CPU = Debug|Any CPU 31 | Release|Any CPU = Release|Any CPU 32 | UnitTest|Any CPU = UnitTest|Any CPU 33 | EndGlobalSection 34 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 35 | {CD0B4E3B-7424-4EF0-A339-F290A658EDC6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 36 | {CD0B4E3B-7424-4EF0-A339-F290A658EDC6}.Debug|Any CPU.Build.0 = Debug|Any CPU 37 | {CD0B4E3B-7424-4EF0-A339-F290A658EDC6}.Release|Any CPU.ActiveCfg = Release|Any CPU 38 | {CD0B4E3B-7424-4EF0-A339-F290A658EDC6}.Release|Any CPU.Build.0 = Release|Any CPU 39 | {CD0B4E3B-7424-4EF0-A339-F290A658EDC6}.UnitTest|Any CPU.ActiveCfg = UnitTest|Any CPU 40 | {CD0B4E3B-7424-4EF0-A339-F290A658EDC6}.UnitTest|Any CPU.Build.0 = UnitTest|Any CPU 41 | {FDB83C93-ED9F-4B31-A172-D0D0FC584FCD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 42 | {FDB83C93-ED9F-4B31-A172-D0D0FC584FCD}.Debug|Any CPU.Build.0 = Debug|Any CPU 43 | {FDB83C93-ED9F-4B31-A172-D0D0FC584FCD}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {FDB83C93-ED9F-4B31-A172-D0D0FC584FCD}.Release|Any CPU.Build.0 = Release|Any CPU 45 | {FDB83C93-ED9F-4B31-A172-D0D0FC584FCD}.UnitTest|Any CPU.ActiveCfg = Release|Any CPU 46 | {FDB83C93-ED9F-4B31-A172-D0D0FC584FCD}.UnitTest|Any CPU.Build.0 = Release|Any CPU 47 | {5739DAB0-143E-4118-8660-98B9DD10971B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 48 | {5739DAB0-143E-4118-8660-98B9DD10971B}.Debug|Any CPU.Build.0 = Debug|Any CPU 49 | {5739DAB0-143E-4118-8660-98B9DD10971B}.Release|Any CPU.ActiveCfg = Release|Any CPU 50 | {5739DAB0-143E-4118-8660-98B9DD10971B}.Release|Any CPU.Build.0 = Release|Any CPU 51 | {5739DAB0-143E-4118-8660-98B9DD10971B}.UnitTest|Any CPU.ActiveCfg = Release|Any CPU 52 | {5739DAB0-143E-4118-8660-98B9DD10971B}.UnitTest|Any CPU.Build.0 = Release|Any CPU 53 | {2E71DCD7-E420-43A5-85A7-01502B8BDAE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 54 | {2E71DCD7-E420-43A5-85A7-01502B8BDAE0}.Debug|Any CPU.Build.0 = Debug|Any CPU 55 | {2E71DCD7-E420-43A5-85A7-01502B8BDAE0}.Release|Any CPU.ActiveCfg = Release|Any CPU 56 | {2E71DCD7-E420-43A5-85A7-01502B8BDAE0}.Release|Any CPU.Build.0 = Release|Any CPU 57 | {2E71DCD7-E420-43A5-85A7-01502B8BDAE0}.UnitTest|Any CPU.ActiveCfg = Release|Any CPU 58 | {2E71DCD7-E420-43A5-85A7-01502B8BDAE0}.UnitTest|Any CPU.Build.0 = Release|Any CPU 59 | {3A90D5D0-AA31-48B2-82BD-50A936C90D6C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 60 | {3A90D5D0-AA31-48B2-82BD-50A936C90D6C}.Debug|Any CPU.Build.0 = Debug|Any CPU 61 | {3A90D5D0-AA31-48B2-82BD-50A936C90D6C}.Release|Any CPU.ActiveCfg = Release|Any CPU 62 | {3A90D5D0-AA31-48B2-82BD-50A936C90D6C}.Release|Any CPU.Build.0 = Release|Any CPU 63 | {3A90D5D0-AA31-48B2-82BD-50A936C90D6C}.UnitTest|Any CPU.ActiveCfg = Release|Any CPU 64 | {3A90D5D0-AA31-48B2-82BD-50A936C90D6C}.UnitTest|Any CPU.Build.0 = Release|Any CPU 65 | {4AB10B28-5DC6-42FE-8966-5CBC4EB19845}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 66 | {4AB10B28-5DC6-42FE-8966-5CBC4EB19845}.Debug|Any CPU.Build.0 = Debug|Any CPU 67 | {4AB10B28-5DC6-42FE-8966-5CBC4EB19845}.Release|Any CPU.ActiveCfg = Release|Any CPU 68 | {4AB10B28-5DC6-42FE-8966-5CBC4EB19845}.Release|Any CPU.Build.0 = Release|Any CPU 69 | {4AB10B28-5DC6-42FE-8966-5CBC4EB19845}.UnitTest|Any CPU.ActiveCfg = Release|Any CPU 70 | {4AB10B28-5DC6-42FE-8966-5CBC4EB19845}.UnitTest|Any CPU.Build.0 = Release|Any CPU 71 | EndGlobalSection 72 | GlobalSection(SolutionProperties) = preSolution 73 | HideSolutionNode = FALSE 74 | EndGlobalSection 75 | GlobalSection(ExtensibilityGlobals) = postSolution 76 | SolutionGuid = {94E9F1F5-C5A9-47E2-8B6A-451879932B08} 77 | EndGlobalSection 78 | EndGlobal 79 | -------------------------------------------------------------------------------- /src/dotnetCampus.Configurations/Core/ConfigurationFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Threading.Tasks; 6 | 7 | namespace dotnetCampus.Configurations.Core 8 | { 9 | /// 10 | /// 为了避免同一个进程中对同一个文件的竞争访问,此类型提供线程安全的获取 的工厂方法。 11 | /// 12 | public static class ConfigurationFactory 13 | { 14 | /// 15 | /// 管理不同文件的 的实例。 16 | /// 17 | private static readonly ConcurrentDictionary> Configurations = new(); 18 | 19 | /// 20 | /// 从文件创建默认的配置管理仓库,你将可以使用类似字典的方式管理线程和进程安全的应用程序配置。 21 | /// 对于同一个文件,此方法会获取到相同的 的实例。 22 | /// 此方法是线程安全的。 23 | /// 24 | /// 来自于本地文件系统的文件名/路径。文件或文件所在的文件夹不需要提前存在。 25 | /// 一个用于管理指定文件的配置仓库。 26 | public static FileConfigurationRepo FromFile(string fileName) => FromFile(fileName, RepoSyncingBehavior.Sync); 27 | 28 | /// 29 | /// 从文件创建默认的配置管理仓库,你将可以使用类似字典的方式管理线程和进程安全的应用程序配置。 30 | /// 对于同一个文件,此方法会获取到相同的 的实例。 31 | /// 此方法是线程安全的。 32 | /// 33 | /// 来自于本地文件系统的文件名/路径。文件或文件所在的文件夹不需要提前存在。 34 | /// 指定应如何读取数据。是实时监听文件变更,还是只读一次,后续不再监听变更。后者性能更好。 35 | /// 一个用于管理指定文件的配置仓库。 36 | public static FileConfigurationRepo FromFile(string fileName, RepoSyncingBehavior syncingBehavior) 37 | { 38 | if (fileName == null) 39 | { 40 | throw new ArgumentNullException(nameof(fileName)); 41 | } 42 | 43 | if (string.IsNullOrWhiteSpace(fileName)) 44 | { 45 | throw new ArgumentException("文件名不能使用空字符串。", nameof(fileName)); 46 | } 47 | 48 | var path = Path.GetFullPath(fileName); 49 | var reference = Configurations.GetOrAdd(new(path, syncingBehavior), CreateConfigurationReference); 50 | 51 | // 以下两个 if 一个 lock 是类似于单例模式的创建方式,既保证性能又保证只创建一次。 52 | if (!reference.TryGetTarget(out var config)) 53 | { 54 | lock (reference) 55 | { 56 | if (!reference.TryGetTarget(out config)) 57 | { 58 | config = CreateConfiguration(path, syncingBehavior); 59 | reference.SetTarget(config); 60 | } 61 | } 62 | } 63 | 64 | return config; 65 | } 66 | 67 | /// 68 | /// 创建 的弱引用实例。 69 | /// 为了保证线程安全,此方法仅能被 访问。 70 | /// 71 | /// 已经过验证的完整文件路径和外部数据同步方式。 72 | /// 的弱引用实例。 73 | private static WeakReference CreateConfigurationReference(RepoKey key) 74 | => new(CreateConfiguration(key.FilePath, key.Syncing)); 75 | 76 | /// 77 | /// 创建 的新实例。 78 | /// 为了保证线程安全,此方法仅能被 访问。 79 | /// 80 | /// 已经过验证的完整文件路径。 81 | /// 指定应如何读取数据。是实时监听文件变更,还是只读一次,后续不再监听变更。后者性能更好。 82 | /// 的新实例。 83 | private static FileConfigurationRepo CreateConfiguration(string path, RepoSyncingBehavior syncingBehavior) 84 | #pragma warning disable CS0618 // 类型或成员已过时 85 | => new(path, syncingBehavior); 86 | #pragma warning restore CS0618 // 类型或成员已过时 87 | 88 | /// 89 | /// 尝试重新加载此配置文件的外部修改(例如使用其他编辑器或其他客户端修改的部分)。 90 | /// 外部修改会自动同步到此配置中,但此同步不会立刻发生,所以如果你明确知道外部修改了文件后需要立刻重新加载外部修改,才需要调用此方法。 91 | /// 92 | public static Task ReloadExternalChangesAsync(this IAppConfigurator configs) 93 | { 94 | if (configs is null) 95 | { 96 | throw new ArgumentNullException(nameof(configs)); 97 | } 98 | 99 | if (configs.Of().Repo is FileConfigurationRepo repo) 100 | { 101 | return repo.ReloadExternalChangesAsync(); 102 | } 103 | return Task.FromResult(null); 104 | } 105 | 106 | private readonly struct RepoKey 107 | { 108 | public RepoKey(string filePath, RepoSyncingBehavior syncing) 109 | { 110 | FilePath = filePath ?? throw new ArgumentNullException(nameof(filePath)); 111 | Syncing = syncing; 112 | } 113 | 114 | public string FilePath { get; } 115 | 116 | public RepoSyncingBehavior Syncing { get; } 117 | 118 | public override bool Equals(object? obj) 119 | { 120 | return obj is RepoKey key && 121 | FilePath == key.FilePath && 122 | Syncing == key.Syncing; 123 | } 124 | 125 | public override int GetHashCode() 126 | { 127 | var hashCode = -527002742; 128 | hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(FilePath); 129 | hashCode = hashCode * -1521134295 + Syncing.GetHashCode(); 130 | return hashCode; 131 | } 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/dotnetCampus.Configurations/Core/CoinConfigurationSerializer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | 7 | namespace dotnetCampus.Configurations.Core 8 | { 9 | /// 10 | /// 配置文件 Coin 序列化 11 | /// 12 | public static class CoinConfigurationSerializer 13 | { 14 | /// 15 | /// 存储的转义 16 | /// 17 | /// 18 | /// 19 | private static string EscapeString(string str) 20 | { 21 | if (str is null) 22 | { 23 | throw new ArgumentNullException(nameof(str)); 24 | } 25 | 26 | // 如果开头是 `>` 就需要转换为 `?>` 27 | // 开头是 `?` 转换为 `??` 28 | 29 | var splitString = _splitString; 30 | var escapeString = _escapeString; 31 | 32 | if (str.StartsWith(splitString, StringComparison.Ordinal)) 33 | { 34 | return _escapeString + str; 35 | } 36 | 37 | if (str.StartsWith(escapeString, StringComparison.Ordinal)) 38 | { 39 | return _escapeString + str; 40 | } 41 | 42 | return str; 43 | } 44 | 45 | /// 46 | /// 将键值对字典序列化为文本字符串。 47 | /// 48 | /// 要序列化的键值对字典。 49 | /// 序列化后的文本字符串。 50 | public static string Serialize(IReadOnlyDictionary keyValue) 51 | { 52 | if (ReferenceEquals(keyValue, null)) throw new ArgumentNullException(nameof(keyValue)); 53 | var keyValuePairList = keyValue.ToArray().OrderBy(p => p.Key); 54 | 55 | return Serialize(keyValuePairList); 56 | } 57 | 58 | /// 59 | /// 将键值对字典序列化为文本字符串。 60 | /// 61 | /// 要序列化的键值对字典。 62 | /// 序列化后的文本字符串。 63 | public static string Serialize(Dictionary keyValue) 64 | { 65 | if (ReferenceEquals(keyValue, null)) throw new ArgumentNullException(nameof(keyValue)); 66 | var keyValuePairList = keyValue.ToArray().OrderBy(p => p.Key); 67 | 68 | return Serialize(keyValuePairList); 69 | } 70 | 71 | private static string Serialize(IOrderedEnumerable> keyValuePairList) 72 | { 73 | var str = new StringBuilder(); 74 | str.Append("> 配置文件\n"); 75 | str.Append("> 版本 1.0\n"); 76 | 77 | foreach (var temp in keyValuePairList) 78 | { 79 | // str.AppendLine 在一些地区使用的是 \r\n 所以不符合反序列化 80 | 81 | str.Append(EscapeString(temp.Key ?? "")); 82 | str.Append("\n"); 83 | str.Append(EscapeString(temp.Value ?? "")); 84 | str.Append("\n>\n"); 85 | } 86 | 87 | str.Append("> 配置文件结束"); 88 | return str.ToString(); 89 | } 90 | 91 | private static string _splitString = ">"; 92 | private static string _escapeString = "?"; 93 | 94 | /// 95 | /// 反序列化的核心实现,反序列化字符串 96 | /// 97 | /// 98 | /// 99 | public static Dictionary Deserialize(string str) 100 | { 101 | if (ReferenceEquals(str, null)) throw new ArgumentNullException(nameof(str)); 102 | var keyValuePairList = str.Split('\n'); 103 | var keyValue = new Dictionary(StringComparer.Ordinal); 104 | string? key = null; 105 | var splitString = _splitString; 106 | 107 | foreach (var temp in keyValuePairList.Select(temp => temp.Trim())) 108 | { 109 | if (temp.StartsWith(splitString, StringComparison.Ordinal)) 110 | { 111 | // 分割,可以作为注释,这一行忽略 112 | // 下一行必须是key 113 | key = null; 114 | continue; 115 | } 116 | 117 | var unescapedString = UnescapeString(temp); 118 | 119 | if (key == null) 120 | { 121 | key = unescapedString; 122 | 123 | // 文件存在多个地方都记录相同的值 124 | // 如果有多个地方记录相同的值,使用最后的值替换前面文件 125 | if (keyValue.ContainsKey(key)) 126 | { 127 | keyValue.Remove(key); 128 | } 129 | } 130 | else 131 | { 132 | if (keyValue.ContainsKey(key)) 133 | { 134 | // key 135 | // v1 136 | // v2 137 | // 返回 {"key","v1\nv2"} 138 | keyValue[key] = keyValue[key] + "\n" + unescapedString; 139 | } 140 | else 141 | { 142 | keyValue.Add(key, unescapedString); 143 | } 144 | } 145 | } 146 | 147 | return keyValue; 148 | } 149 | 150 | /// 151 | /// 存储的反转义 152 | /// 153 | /// 154 | /// 155 | private static string UnescapeString(string str) 156 | { 157 | var escapeString = _escapeString; 158 | 159 | if (str.StartsWith(escapeString, StringComparison.Ordinal)) 160 | { 161 | return str.Substring(1); 162 | } 163 | 164 | return str; 165 | } 166 | } 167 | } -------------------------------------------------------------------------------- /src/dotnetCampus.Configurations/Extensions/CommandLineConfigurationProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace dotnetCampus.Configurations.Extensions 5 | { 6 | internal class CommandLineConfigurationProvider 7 | { 8 | /// 9 | /// Initializes a new instance. 10 | /// 11 | /// The command line args. 12 | /// The switch mappings. 13 | public CommandLineConfigurationProvider(IEnumerable args, IDictionary? switchMappings = null) 14 | { 15 | Args = args ?? throw new ArgumentNullException(nameof(args)); 16 | 17 | if (switchMappings != null) 18 | { 19 | _switchMappings = GetValidatedSwitchMappingsCopy(switchMappings); 20 | } 21 | } 22 | 23 | /// 24 | /// The command line arguments. 25 | /// 26 | protected IEnumerable Args { get; private set; } 27 | 28 | private readonly Dictionary? _switchMappings; 29 | 30 | /// 31 | /// Loads the configuration data from the command line args. 32 | /// 33 | public Dictionary Load() 34 | { 35 | var data = new Dictionary(StringComparer.OrdinalIgnoreCase); 36 | string key, value; 37 | 38 | using (var enumerator = Args.GetEnumerator()) 39 | { 40 | while (enumerator.MoveNext()) 41 | { 42 | var currentArg = enumerator.Current; 43 | var keyStartIndex = 0; 44 | 45 | if (currentArg.StartsWith("--", StringComparison.OrdinalIgnoreCase)) 46 | { 47 | keyStartIndex = 2; 48 | } 49 | else if (currentArg.StartsWith("-", StringComparison.OrdinalIgnoreCase)) 50 | { 51 | keyStartIndex = 1; 52 | } 53 | else if (currentArg.StartsWith("/", StringComparison.OrdinalIgnoreCase)) 54 | { 55 | // "/SomeSwitch" is equivalent to "--SomeSwitch" when interpreting switch mappings 56 | // So we do a conversion to simplify later processing 57 | currentArg = $"--{currentArg.Substring(1)}"; 58 | keyStartIndex = 2; 59 | } 60 | 61 | var separator = currentArg.IndexOf('='); 62 | 63 | if (separator < 0) 64 | { 65 | // If there is neither equal sign nor prefix in current arugment, it is an invalid format 66 | if (keyStartIndex == 0) 67 | { 68 | // Ignore invalid formats 69 | continue; 70 | } 71 | 72 | // If the switch is a key in given switch mappings, interpret it 73 | if (_switchMappings != null && _switchMappings.TryGetValue(currentArg, out var mappedKey)) 74 | { 75 | key = mappedKey; 76 | } 77 | // If the switch starts with a single "-" and it isn't in given mappings , it is an invalid usage so ignore it 78 | else if (keyStartIndex == 1) 79 | { 80 | continue; 81 | } 82 | // Otherwise, use the switch name directly as a key 83 | else 84 | { 85 | key = currentArg.Substring(keyStartIndex); 86 | } 87 | 88 | var previousKey = enumerator.Current; 89 | if (!enumerator.MoveNext()) 90 | { 91 | // ignore missing values 92 | continue; 93 | } 94 | 95 | value = enumerator.Current; 96 | } 97 | else 98 | { 99 | var keySegment = currentArg.Substring(0, separator); 100 | 101 | // If the switch is a key in given switch mappings, interpret it 102 | if (_switchMappings != null && _switchMappings.TryGetValue(keySegment, out var mappedKeySegment)) 103 | { 104 | key = mappedKeySegment; 105 | } 106 | // If the switch starts with a single "-" and it isn't in given mappings , it is an invalid usage 107 | else if (keyStartIndex == 1) 108 | { 109 | throw new FormatException($"Short Switch {currentArg} Not Defined"); 110 | } 111 | // Otherwise, use the switch name directly as a key 112 | else 113 | { 114 | key = currentArg.Substring(keyStartIndex, separator - keyStartIndex); 115 | } 116 | 117 | value = currentArg.Substring(separator + 1); 118 | } 119 | 120 | // Override value when key is duplicated. So we always have the last argument win. 121 | data[key] = value; 122 | } 123 | } 124 | 125 | //Data = data; 126 | return data; 127 | } 128 | 129 | private Dictionary GetValidatedSwitchMappingsCopy(IDictionary switchMappings) 130 | { 131 | // The dictionary passed in might be constructed with a case-sensitive comparer 132 | // However, the keys in configuration providers are all case-insensitive 133 | // So we check whether the given switch mappings contain duplicated keys with case-insensitive comparer 134 | var switchMappingsCopy = 135 | new Dictionary(switchMappings.Count, StringComparer.OrdinalIgnoreCase); 136 | foreach (var mapping in switchMappings) 137 | { 138 | // Only keys start with "--" or "-" are acceptable 139 | if (!mapping.Key.StartsWith("-", StringComparison.Ordinal) && !mapping.Key.StartsWith("--", StringComparison.Ordinal)) 140 | { 141 | throw new ArgumentException($"The map key should start with `-` or `--`, but the current key is {mapping.Key}", 142 | nameof(switchMappings)); 143 | } 144 | 145 | if (switchMappingsCopy.ContainsKey(mapping.Key)) 146 | { 147 | throw new ArgumentException( 148 | $"Duplicated Key In SwitchMappings, the duplicated key is {mapping.Key}", 149 | nameof(switchMappings)); 150 | } 151 | 152 | switchMappingsCopy.Add(mapping.Key, mapping.Value); 153 | } 154 | 155 | return switchMappingsCopy; 156 | } 157 | } 158 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | [Aa][Rr][Mm]/ 24 | [Aa][Rr][Mm]64/ 25 | bld/ 26 | [Bb]in/ 27 | [Oo]bj/ 28 | [Ll]og/ 29 | 30 | # Visual Studio 2015/2017 cache/options directory 31 | .vs/ 32 | # Uncomment if you have tasks that create the project's static files in wwwroot 33 | #wwwroot/ 34 | 35 | # Visual Studio 2017 auto generated files 36 | Generated\ Files/ 37 | 38 | # MSTest test Results 39 | [Tt]est[Rr]esult*/ 40 | [Bb]uild[Ll]og.* 41 | 42 | # NUNIT 43 | *.VisualState.xml 44 | TestResult.xml 45 | 46 | # Build Results of an ATL Project 47 | [Dd]ebugPS/ 48 | [Rr]eleasePS/ 49 | dlldata.c 50 | 51 | # Benchmark Results 52 | BenchmarkDotNet.Artifacts/ 53 | 54 | # .NET Core 55 | project.lock.json 56 | project.fragment.lock.json 57 | artifacts/ 58 | 59 | # StyleCop 60 | StyleCopReport.xml 61 | 62 | # Files built by Visual Studio 63 | *_i.c 64 | *_p.c 65 | *_h.h 66 | *.ilk 67 | *.meta 68 | *.obj 69 | *.iobj 70 | *.pch 71 | *.pdb 72 | *.ipdb 73 | *.pgc 74 | *.pgd 75 | *.rsp 76 | *.sbr 77 | *.tlb 78 | *.tli 79 | *.tlh 80 | *.tmp 81 | *.tmp_proj 82 | *_wpftmp.csproj 83 | *.log 84 | *.vspscc 85 | *.vssscc 86 | .builds 87 | *.pidb 88 | *.svclog 89 | *.scc 90 | 91 | # Chutzpah Test files 92 | _Chutzpah* 93 | 94 | # Visual C++ cache files 95 | ipch/ 96 | *.aps 97 | *.ncb 98 | *.opendb 99 | *.opensdf 100 | *.sdf 101 | *.cachefile 102 | *.VC.db 103 | *.VC.VC.opendb 104 | 105 | # Visual Studio profiler 106 | *.psess 107 | *.vsp 108 | *.vspx 109 | *.sap 110 | 111 | # Visual Studio Trace Files 112 | *.e2e 113 | 114 | # TFS 2012 Local Workspace 115 | $tf/ 116 | 117 | # Guidance Automation Toolkit 118 | *.gpState 119 | 120 | # ReSharper is a .NET coding add-in 121 | _ReSharper*/ 122 | *.[Rr]e[Ss]harper 123 | *.DotSettings.user 124 | 125 | # JustCode is a .NET coding add-in 126 | .JustCode 127 | 128 | # TeamCity is a build add-in 129 | _TeamCity* 130 | 131 | # DotCover is a Code Coverage Tool 132 | *.dotCover 133 | 134 | # AxoCover is a Code Coverage Tool 135 | .axoCover/* 136 | !.axoCover/settings.json 137 | 138 | # Visual Studio code coverage results 139 | *.coverage 140 | *.coveragexml 141 | 142 | # NCrunch 143 | _NCrunch_* 144 | .*crunch*.local.xml 145 | nCrunchTemp_* 146 | 147 | # MightyMoose 148 | *.mm.* 149 | AutoTest.Net/ 150 | 151 | # Web workbench (sass) 152 | .sass-cache/ 153 | 154 | # Installshield output folder 155 | [Ee]xpress/ 156 | 157 | # DocProject is a documentation generator add-in 158 | DocProject/buildhelp/ 159 | DocProject/Help/*.HxT 160 | DocProject/Help/*.HxC 161 | DocProject/Help/*.hhc 162 | DocProject/Help/*.hhk 163 | DocProject/Help/*.hhp 164 | DocProject/Help/Html2 165 | DocProject/Help/html 166 | 167 | # Click-Once directory 168 | publish/ 169 | 170 | # Publish Web Output 171 | *.[Pp]ublish.xml 172 | *.azurePubxml 173 | # Note: Comment the next line if you want to checkin your web deploy settings, 174 | # but database connection strings (with potential passwords) will be unencrypted 175 | *.pubxml 176 | *.publishproj 177 | 178 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 179 | # checkin your Azure Web App publish settings, but sensitive information contained 180 | # in these scripts will be unencrypted 181 | PublishScripts/ 182 | 183 | # NuGet Packages 184 | *.nupkg 185 | # The packages folder can be ignored because of Package Restore 186 | **/[Pp]ackages/* 187 | # except build/, which is used as an MSBuild target. 188 | !**/[Pp]ackages/build/ 189 | # Uncomment if necessary however generally it will be regenerated when needed 190 | #!**/[Pp]ackages/repositories.config 191 | # NuGet v3's project.json files produces more ignorable files 192 | *.nuget.props 193 | *.nuget.targets 194 | 195 | # Microsoft Azure Build Output 196 | csx/ 197 | *.build.csdef 198 | 199 | # Microsoft Azure Emulator 200 | ecf/ 201 | rcf/ 202 | 203 | # Windows Store app package directories and files 204 | AppPackages/ 205 | BundleArtifacts/ 206 | Package.StoreAssociation.xml 207 | _pkginfo.txt 208 | *.appx 209 | 210 | # Visual Studio cache files 211 | # files ending in .cache can be ignored 212 | *.[Cc]ache 213 | # but keep track of directories ending in .cache 214 | !?*.[Cc]ache/ 215 | 216 | # Others 217 | ClientBin/ 218 | ~$* 219 | *~ 220 | *.dbmdl 221 | *.dbproj.schemaview 222 | *.jfm 223 | *.pfx 224 | *.publishsettings 225 | orleans.codegen.cs 226 | 227 | # Including strong name files can present a security risk 228 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 229 | #*.snk 230 | 231 | # Since there are multiple workflows, uncomment next line to ignore bower_components 232 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 233 | #bower_components/ 234 | 235 | # RIA/Silverlight projects 236 | Generated_Code/ 237 | 238 | # Backup & report files from converting an old project file 239 | # to a newer Visual Studio version. Backup files are not needed, 240 | # because we have git ;-) 241 | _UpgradeReport_Files/ 242 | Backup*/ 243 | UpgradeLog*.XML 244 | UpgradeLog*.htm 245 | ServiceFabricBackup/ 246 | *.rptproj.bak 247 | 248 | # SQL Server files 249 | *.mdf 250 | *.ldf 251 | *.ndf 252 | 253 | # Business Intelligence projects 254 | *.rdl.data 255 | *.bim.layout 256 | *.bim_*.settings 257 | *.rptproj.rsuser 258 | *- Backup*.rdl 259 | 260 | # Microsoft Fakes 261 | FakesAssemblies/ 262 | 263 | # GhostDoc plugin setting file 264 | *.GhostDoc.xml 265 | 266 | # Node.js Tools for Visual Studio 267 | .ntvs_analysis.dat 268 | node_modules/ 269 | 270 | # Visual Studio 6 build log 271 | *.plg 272 | 273 | # Visual Studio 6 workspace options file 274 | *.opt 275 | 276 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 277 | *.vbw 278 | 279 | # Visual Studio LightSwitch build output 280 | **/*.HTMLClient/GeneratedArtifacts 281 | **/*.DesktopClient/GeneratedArtifacts 282 | **/*.DesktopClient/ModelManifest.xml 283 | **/*.Server/GeneratedArtifacts 284 | **/*.Server/ModelManifest.xml 285 | _Pvt_Extensions 286 | 287 | # Paket dependency manager 288 | .paket/paket.exe 289 | paket-files/ 290 | 291 | # FAKE - F# Make 292 | .fake/ 293 | 294 | # JetBrains Rider 295 | .idea/ 296 | *.sln.iml 297 | 298 | # CodeRush personal settings 299 | .cr/personal 300 | 301 | # Python Tools for Visual Studio (PTVS) 302 | __pycache__/ 303 | *.pyc 304 | 305 | # Cake - Uncomment if you are using it 306 | # tools/** 307 | # !tools/packages.config 308 | 309 | # Tabs Studio 310 | *.tss 311 | 312 | # Telerik's JustMock configuration file 313 | *.jmconfig 314 | 315 | # BizTalk build output 316 | *.btp.cs 317 | *.btm.cs 318 | *.odx.cs 319 | *.xsd.cs 320 | 321 | # OpenCover UI analysis results 322 | OpenCover/ 323 | 324 | # Azure Stream Analytics local run output 325 | ASALocalRun/ 326 | 327 | # MSBuild Binary and Structured Log 328 | *.binlog 329 | 330 | # NVidia Nsight GPU debugger configuration file 331 | *.nvuser 332 | 333 | # MFractors (Xamarin productivity tool) working folder 334 | .mfractor/ 335 | 336 | # Local History for Visual Studio 337 | .localhistory/ 338 | 339 | # BeatPulse healthcheck temp database 340 | healthchecksdb -------------------------------------------------------------------------------- /src/dotnetCampus.Configurations/Converters/ConfigurationExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.Globalization; 4 | using System.Runtime.CompilerServices; 5 | 6 | namespace dotnetCampus.Configurations.Converters 7 | { 8 | /// 9 | /// 为 提供包含配置项值转换的扩展方法。 10 | /// 11 | public static class ConfigurationExtensions 12 | { 13 | /// 14 | /// 在 的派生类中为非基本类型属性的 get 访问器提供获取配置值的方法。 15 | /// 此方法会返回可空值类型,如果配置项不存在,则指为 null。 16 | /// 17 | /// 值类型。 18 | /// 配置项组派生类的实例。 19 | /// 配置项的标识符,自动从属性名中获取。 20 | /// 配置项的值。 21 | public static T? GetValue(this Configuration @this, [CallerMemberName] string? key = null) 22 | where T : struct 23 | { 24 | if (@this == null) 25 | { 26 | throw new ArgumentNullException(nameof(@this)); 27 | } 28 | 29 | if (key is null) 30 | { 31 | throw new ArgumentNullException(nameof(key)); 32 | } 33 | 34 | var type = typeof(T); 35 | var value = @this.GetValue(key); 36 | 37 | // 如果读取到的配置值为 null,则返回 null。 38 | if (string.IsNullOrWhiteSpace(value)) 39 | { 40 | return null; 41 | } 42 | 43 | // 如果读取到的配置值不为 null,则尝试转换。 44 | var typeConverter = TypeDescriptor.GetConverter(type); 45 | var convertedValue = typeConverter.ConvertFromInvariantString(value); 46 | 47 | if (convertedValue == null) 48 | { 49 | // 此分支不可能进入。 50 | throw new NotSupportedException( 51 | $"出现了不可能的情况,从字符串 {value} 转换为值类型 {typeof(T).FullName} 时,转换后的值为 null。"); 52 | } 53 | 54 | return (T) convertedValue; 55 | } 56 | 57 | /// 58 | /// 在派生类中为非基本类型属性的 set 访问器提供设置配置值的方法。 59 | /// 60 | /// 需要设置非基本类型值的配置项组。 61 | /// 配置项的值。 62 | /// 配置项的标识符,自动从属性名中获取。 63 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 64 | public static void SetValue(this Configuration @this, 65 | T value, [CallerMemberName] string? key = null) where T : struct 66 | => SetValue(@this, (T?)value, key); 67 | 68 | /// 69 | /// 在派生类中为非基本类型属性的 set 访问器提供设置配置值的方法。 70 | /// 71 | /// 需要设置非基本类型值的配置项组。 72 | /// 配置项的值。 73 | /// 配置项的标识符,自动从属性名中获取。 74 | public static void SetValue(this Configuration @this, 75 | T? value, [CallerMemberName] string? key = null) where T : struct 76 | { 77 | if (@this == null) 78 | { 79 | throw new ArgumentNullException(nameof(@this)); 80 | } 81 | 82 | if (key is null) 83 | { 84 | throw new ArgumentNullException(nameof(key)); 85 | } 86 | 87 | var type = typeof(T); 88 | 89 | if (value == null) 90 | { 91 | @this.SetValue(string.Empty, key); 92 | } 93 | else 94 | { 95 | // 如果需要设置的配置值不为 null,则尝试转换。 96 | var typeConverter = TypeDescriptor.GetConverter(type); 97 | var convertedValue = typeConverter.ConvertToInvariantString(value.Value); 98 | if (convertedValue == null) 99 | { 100 | throw new NotSupportedException($"无法从类型 {type.FullName} 将值 {value.Value} 转换成字符串。"); 101 | } 102 | 103 | @this.SetValue(convertedValue, key); 104 | } 105 | } 106 | 107 | /// 108 | /// 在派生类中为属性的 set 访问器提供设置配置值的方法。 109 | /// 说明:若要在 get 访问器中获取值,请使用 110 | /// 111 | /// 需要设置非基本类型值的配置项组。 112 | /// 配置项的值。 113 | /// 配置项的标识符,自动从属性名中获取。 114 | [Obsolete("请改用 DateTimeOffset 类型,因为 DateTime 在存储和传输过程中会丢失时区信息导致值在读写后发生变化。")] 115 | public static void SetValue(this Configuration @this, 116 | DateTime value, [CallerMemberName] string? key = null) 117 | => SetValueCore(@this, key, value.ToString("O", CultureInfo.InvariantCulture)); 118 | 119 | /// 120 | /// 在派生类中为属性的 set 访问器提供设置配置值的方法。 121 | /// 说明:若要在 get 访问器中获取值,请使用 122 | /// 123 | /// 需要设置非基本类型值的配置项组。 124 | /// 配置项的值。 125 | /// 配置项的标识符,自动从属性名中获取。 126 | public static void SetValue(this Configuration @this, 127 | DateTimeOffset value, [CallerMemberName] string? key = null) 128 | => SetValueCore(@this, key, value.ToString("O", CultureInfo.InvariantCulture)); 129 | 130 | /// 131 | /// 在派生类中为属性的 set 访问器提供设置配置值的方法。 132 | /// 说明:若要在 get 访问器中获取值,请使用 133 | /// 134 | /// 需要设置非基本类型值的配置项组。 135 | /// 配置项的值。 136 | /// 配置项的标识符,自动从属性名中获取。 137 | public static void SetValue(this Configuration @this, 138 | DateTimeOffset? value, [CallerMemberName] string? key = null) 139 | => SetValueCore(@this, key, value?.ToString("O", CultureInfo.InvariantCulture)); 140 | 141 | /// 142 | /// 在派生类中为属性的 set 访问器提供设置配置值的方法。 143 | /// 说明:若要在 get 访问器中获取值,请使用 144 | /// 145 | /// 需要设置非基本类型值的配置项组。 146 | /// 配置项的值。 147 | /// 配置项的标识符,自动从属性名中获取。 148 | public static void SetValue(this Configuration @this, 149 | TimeSpan value, [CallerMemberName] string? key = null) 150 | => SetValueCore(@this, key, value.ToString("O", CultureInfo.InvariantCulture)); 151 | 152 | /// 153 | /// 在派生类中为属性的 set 访问器提供设置配置值的方法。 154 | /// 说明:若要在 get 访问器中获取值,请使用 155 | /// 156 | /// 需要设置非基本类型值的配置项组。 157 | /// 配置项的值。 158 | /// 配置项的标识符,自动从属性名中获取。 159 | public static void SetValue(this Configuration @this, 160 | TimeSpan? value, [CallerMemberName] string? key = null) 161 | => SetValueCore(@this, key, value?.ToString("O", CultureInfo.InvariantCulture)); 162 | 163 | /// 164 | /// 在转换器的扩展方法中用于简化设置值的扩展方法。 165 | /// 166 | /// 需要设置非基本类型值的配置项组。 167 | /// 配置项的标识符。 168 | /// 已经转换好的字符串。 169 | private static void SetValueCore(Configuration configs, string? key, string? value) 170 | { 171 | if (configs == null) 172 | { 173 | throw new ArgumentNullException(nameof(configs)); 174 | } 175 | 176 | if (key is null) 177 | { 178 | throw new ArgumentNullException(nameof(key)); 179 | } 180 | 181 | configs.SetValue(value, key); 182 | } 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/dotnetCampus.Configurations/IO/FileWatcher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Runtime.InteropServices; 4 | using System.Threading.Tasks; 5 | using dotnetCampus.Configurations.IO; 6 | using dotnetCampus.Configurations.Utils; 7 | 8 | #pragma warning disable CA1001 // Types that own disposable fields should be disposable 9 | 10 | namespace dotnetCampus.IO 11 | { 12 | /// 13 | /// 监视特定文件内容的改变。 14 | /// 即使文件或文件夹不存在也能在其将来被创建的时候监视;或者如果删除,将在其下次创建的时候监视。 15 | /// 16 | public sealed class FileWatcher 17 | { 18 | /// 监视的文件。 19 | private readonly FileInfo _file; 20 | 21 | /// 获取当前监视的文件夹监视器。可能会因为文件(夹)不存在而改变。 22 | private FileSystemWatcher? _watcher; 23 | 24 | /// 当前正在等待的异步任务。 25 | private Task? _waitingAsyncAction; 26 | 27 | /// 28 | /// 创建用于监视 。 29 | /// 30 | /// 要监视文件的完全限定路径。 31 | public FileWatcher(string fileName) => 32 | _file = new FileInfo(fileName ?? throw new ArgumentNullException(nameof(fileName))); 33 | 34 | /// 35 | /// 创建用于监视 。 36 | /// 37 | /// 要监视的文件。 38 | public FileWatcher(FileInfo file) => _file = file ?? throw new ArgumentNullException(nameof(file)); 39 | 40 | /// 41 | /// 当要监视的文件的内容改变的时候发生事件。 42 | /// 43 | public event EventHandler? Changed; 44 | 45 | private void OnChanged() => Changed?.Invoke(this, EventArgs.Empty); 46 | 47 | /// 48 | /// 异步开始监视文件的改变。 49 | /// 50 | /// 51 | /// 此方法可以被重复调用,不会引发异常或导致重复监视。 52 | /// 53 | public async Task WatchAsync() 54 | { 55 | await WaitPendingTaskAsync().ConfigureAwait(false); 56 | _waitingAsyncAction = Task.Run(Watch); 57 | await _waitingAsyncAction.ConfigureAwait(false); 58 | } 59 | 60 | /// 61 | /// 异步开始监视文件的改变。 62 | /// 63 | /// 64 | /// 此方法可以被重复调用,不会引发异常或导致重复监视。 65 | /// 66 | public async Task StopAsync() 67 | { 68 | await WaitPendingTaskAsync().ConfigureAwait(false); 69 | _waitingAsyncAction = null; 70 | Stop(); 71 | } 72 | 73 | /// 74 | /// 监视文件的改变。 75 | /// 最多重试10次。 76 | /// 77 | /// 78 | /// 此方法可以被重复调用,不会引发异常或导致重复监视。 79 | /// 80 | private void Watch() => Watch(10); 81 | 82 | /// 83 | /// 监视文件的改变。 84 | /// 85 | /// 86 | /// 重试次数 87 | /// 88 | /// 89 | /// 此方法可以被重复调用,不会引发异常或导致重复监视。 90 | /// 91 | private void Watch(int retryTime) 92 | { 93 | Stop(); 94 | 95 | retryTime--; 96 | if (retryTime <= 0) //限制次数,防止爆栈 97 | { 98 | return; 99 | } 100 | 101 | var pair = FindWatchableLevel(); 102 | var directory = pair._directory; 103 | var file = pair._file; 104 | 105 | if (string.IsNullOrEmpty(directory)) 106 | { 107 | //文件的上级目录找不到,放弃了 108 | //比如文件所在的盘符不存在了 109 | return; 110 | } 111 | 112 | try 113 | { 114 | if (File.Exists(_file.FullName)) 115 | { 116 | // 如果文件存在,说明这是最终的文件。 117 | // 注意使用 File.Exists 判断已存在的同名文件夹时会返回 false。 118 | _watcher = new FileSystemWatcher(directory, file) 119 | { 120 | EnableRaisingEvents = true, 121 | NotifyFilter = 122 | // 文件被修改 123 | NotifyFilters.LastWrite 124 | // 文件被删除 125 | | NotifyFilters.FileName, 126 | }; 127 | var weakEvent = new FileSystemWatcherWeakEventRelay(_watcher); 128 | weakEvent.Changed += FinalFile_Changed; 129 | weakEvent.Deleted += FileOrDirectory_CreatedOrDeleted; 130 | } 131 | else 132 | { 133 | // 注意这里的 file 可能是文件也可能是文件夹。 134 | _watcher = new FileSystemWatcher(directory, file) 135 | { 136 | EnableRaisingEvents = true, 137 | }; 138 | var weakEvent = new FileSystemWatcherWeakEventRelay(_watcher); 139 | weakEvent.Created += FileOrDirectory_CreatedOrDeleted; 140 | weakEvent.Renamed += FileOrDirectory_CreatedOrDeleted; 141 | weakEvent.Deleted += FileOrDirectory_CreatedOrDeleted; 142 | } 143 | } 144 | catch (ArgumentException) 145 | { 146 | // 构造函数抛出:目录名无效。 147 | // 在 FindWatchableLevel 找到上级目录之后,到执行到这里上级目录又被删除了,重试。 148 | Watch(retryTime); 149 | } 150 | catch (FileNotFoundException) 151 | { 152 | // EnableRaisingEvents 属性设置抛出 153 | // 在 FindWatchableLevel 找到上级目录之后,到执行到这里上级目录又被删除了,重试。 154 | Watch(retryTime); 155 | } 156 | } 157 | 158 | /// 159 | /// 停止监视文件的改变。 160 | /// 161 | /// 162 | /// 此方法可以被重复调用。 163 | /// 164 | private void Stop() 165 | { 166 | // 文件 / 文件夹已经创建,所以之前的监视不需要了。 167 | // 文件 / 文件夹被删除了,所以之前的监视没法儿用了。 168 | // Dispose 之后,这个对象就没用了,事件也不会再引发,所以不需要注销事件。 169 | _watcher?.Dispose(); 170 | _watcher = null; 171 | } 172 | 173 | private void FileOrDirectory_CreatedOrDeleted(object sender, FileSystemEventArgs e) 174 | { 175 | CT.Log($"[文件 {e.ChangeType}]", _file.Name); 176 | 177 | // 当文件创建或删除之后,需要重新设置监听方式。 178 | Watch(); 179 | 180 | // 文件创建或删除也是文件内容改变的一种(0 字节变多或者文件内容变 0 字节),通知调用者文件内容已经发生改变。 181 | OnChanged(); 182 | } 183 | 184 | private void FinalFile_Changed(object sender, FileSystemEventArgs e) 185 | { 186 | CT.Log($"[文件 Changed]", _file.Name); 187 | OnChanged(); 188 | } 189 | 190 | /// 191 | /// 从 开始寻找第一层存在的文件夹,返回里面的文件。 192 | /// 193 | /// 194 | private FolderPair FindWatchableLevel() 195 | { 196 | var path = _file.FullName; 197 | 198 | // 如果文件存在,就返回文件所在的文件夹和文件本身。 199 | if (File.Exists(path)) 200 | { 201 | return new FolderPair(Path.GetDirectoryName(path), Path.GetFileName(path)); 202 | } 203 | 204 | // 如果文件不存在,但文件夹存在,也是返回文件夹和文件本身。 205 | // 这一点在下面的第一层循环中体现。 206 | 207 | // 对于每一层循环。 208 | while (true) 209 | { 210 | var directory = Path.GetDirectoryName(path); 211 | var file = Path.GetFileName(path); 212 | 213 | // 如果找不到上级目录了,直接返回 214 | if (string.IsNullOrEmpty(directory)) 215 | { 216 | return new FolderPair(directory, file); 217 | } 218 | 219 | // 检查文件夹是否存在,只要文件夹存在,那么就可以返回。 220 | if (Directory.Exists(directory)) 221 | { 222 | return new FolderPair(directory, file); 223 | } 224 | 225 | // 如果连文件夹都不存在,那么就需要查找上一层文件夹。 226 | path = directory; 227 | } 228 | } 229 | 230 | /// 231 | /// 等待当前未完成的任务直到完成。 232 | /// 233 | private async Task WaitPendingTaskAsync() 234 | { 235 | if (_waitingAsyncAction != null) 236 | { 237 | await _waitingAsyncAction.ConfigureAwait(false); 238 | } 239 | } 240 | 241 | [StructLayout(LayoutKind.Auto)] 242 | private readonly struct FolderPair 243 | { 244 | internal readonly string? _directory; 245 | internal readonly string? _file; 246 | 247 | public FolderPair(string? directory, string? file) 248 | { 249 | _directory = directory; 250 | _file = file; 251 | } 252 | } 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /src/dotnetCampus.Configurations/Extensions/CommandLineConfigurationExtensions.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace dotnetCampus.Configurations.Extensions 6 | { 7 | /// 8 | /// Extension methods for command line 9 | /// 10 | /// copy https://github.com/dotnet/runtime/blob/4855a4cb67717c15e377a42c450750b769ce0601/src/libraries/Microsoft.Extensions.Configuration.CommandLine/src/CommandLineConfigurationExtensions.cs 11 | public static class CommandLineConfigurationExtensions 12 | { 13 | /// 14 | /// Adds a 15 | /// that reads configuration values from the command line. 16 | /// 17 | /// The to add to. 18 | /// The command line args. 19 | /// The . 20 | /// 21 | /// 22 | /// The values passed on the command line, in the args string array, should be a set 23 | /// of keys prefixed with two dashes ("--") and then values, separate by either the 24 | /// equals sign ("=") or a space (" "). 25 | /// 26 | /// 27 | /// A forward slash ("/") can be used as an alternative prefix, with either equals or space, and when using 28 | /// an equals sign the prefix can be left out altogether. 29 | /// 30 | /// 31 | /// There are five basic alternative formats for arguments: 32 | /// key1=value1 --key2=value2 /key3=value3 --key4 value4 /key5 value5. 33 | /// 34 | /// 35 | /// 36 | /// A simple console application that has five values. 37 | /// 38 | /// // dotnet run key1=value1 --key2=value2 /key3=value3 --key4 value4 /key5 value5 39 | /// 40 | /// using Microsoft.Extensions.Configuration; 41 | /// using System; 42 | /// 43 | /// namespace CommandLineSample 44 | /// { 45 | /// public class Program 46 | /// { 47 | /// public static void Main(string[] args) 48 | /// { 49 | /// var builder = new ConfigurationBuilder(); 50 | /// builder.AddCommandLine(args); 51 | /// 52 | /// var config = builder.Build(); 53 | /// 54 | /// Console.WriteLine($"Key1: '{config["Key1"]}'"); 55 | /// Console.WriteLine($"Key2: '{config["Key2"]}'"); 56 | /// Console.WriteLine($"Key3: '{config["Key3"]}'"); 57 | /// Console.WriteLine($"Key4: '{config["Key4"]}'"); 58 | /// Console.WriteLine($"Key5: '{config["Key5"]}'"); 59 | /// } 60 | /// } 61 | /// } 62 | /// 63 | /// 64 | public static IAppConfigurator AddCommandLine(this IAppConfigurator appConfigurator, 65 | string[] args) 66 | { 67 | return appConfigurator.AddCommandLine(args, switchMappings: null); 68 | } 69 | 70 | /// 71 | /// Adds a that reads 72 | /// configuration values from the command line using the specified switch mappings. 73 | /// 74 | /// The to add to. 75 | /// The command line args. 76 | /// 77 | /// The switch mappings. A dictionary of short (with prefix "-") and 78 | /// alias keys (with prefix "--"), mapped to the configuration key (no prefix). 79 | /// 80 | /// The . 81 | /// 82 | /// 83 | /// The switchMappings allows additional formats for alternative short and alias keys 84 | /// to be used from the command line. Also see the basic version of AddCommandLine for 85 | /// the standard formats supported. 86 | /// 87 | /// 88 | /// Short keys start with a single dash ("-") and are mapped to the main key name (without 89 | /// prefix), and can be used with either equals or space. The single dash mappings are intended 90 | /// to be used for shorter alternative switches. 91 | /// 92 | /// 93 | /// Note that a single dash switch cannot be accessed directly, but must have a switch mapping 94 | /// defined and accessed using the full key. Passing an undefined single dash argument will 95 | /// cause as FormatException. 96 | /// 97 | /// 98 | /// There are two formats for short arguments: 99 | /// -k1=value1 -k2 value2. 100 | /// 101 | /// 102 | /// Alias key definitions start with two dashes ("--") and are mapped to the main key name (without 103 | /// prefix), and can be used in place of the normal key. They also work when a forward slash prefix 104 | /// is used in the command line (but not with the no prefix equals format). 105 | /// 106 | /// 107 | /// There are only four formats for aliased arguments: 108 | /// --alt3=value3 /alt4=value4 --alt5 value5 /alt6 value6. 109 | /// 110 | /// 111 | /// 112 | /// A simple console application that has two short and four alias switch mappings defined. 113 | /// 114 | /// // dotnet run -k1=value1 -k2 value2 --alt3=value2 /alt4=value3 --alt5 value5 /alt6 value6 115 | /// 116 | /// using Microsoft.Extensions.Configuration; 117 | /// using System; 118 | /// using System.Collections.Generic; 119 | /// 120 | /// namespace CommandLineSample 121 | /// { 122 | /// public class Program 123 | /// { 124 | /// public static void Main(string[] args) 125 | /// { 126 | /// var switchMappings = new Dictionary<string, string>() 127 | /// { 128 | /// { "-k1", "key1" }, 129 | /// { "-k2", "key2" }, 130 | /// { "--alt3", "key3" }, 131 | /// { "--alt4", "key4" }, 132 | /// { "--alt5", "key5" }, 133 | /// { "--alt6", "key6" }, 134 | /// }; 135 | /// var builder = new ConfigurationBuilder(); 136 | /// builder.AddCommandLine(args, switchMappings); 137 | /// 138 | /// var config = builder.Build(); 139 | /// 140 | /// Console.WriteLine($"Key1: '{config["Key1"]}'"); 141 | /// Console.WriteLine($"Key2: '{config["Key2"]}'"); 142 | /// Console.WriteLine($"Key3: '{config["Key3"]}'"); 143 | /// Console.WriteLine($"Key4: '{config["Key4"]}'"); 144 | /// Console.WriteLine($"Key5: '{config["Key5"]}'"); 145 | /// Console.WriteLine($"Key6: '{config["Key6"]}'"); 146 | /// } 147 | /// } 148 | /// } 149 | /// 150 | /// 151 | public static IAppConfigurator AddCommandLine( 152 | this IAppConfigurator appConfigurator, 153 | string[] args, 154 | IDictionary? switchMappings) 155 | { 156 | var commandLineConfigurationProvider = new CommandLineConfigurationProvider(args, switchMappings); 157 | var dictionary = commandLineConfigurationProvider.Load(); 158 | foreach (var keyValuePair in dictionary) 159 | { 160 | appConfigurator.Default.SetValue(keyValuePair.Key, keyValuePair.Value); 161 | } 162 | 163 | return appConfigurator; 164 | } 165 | } 166 | 167 | // copy from https://github.com/dotnet/runtime/blob/4855a4cb67717c15e377a42c450750b769ce0601/src/libraries/Microsoft.Extensions.Configuration.CommandLine/src/CommandLineConfigurationProvider.cs 168 | } 169 | -------------------------------------------------------------------------------- /src/dotnetCampus.Configurations/Core/AsynchronousConfigurationRepo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics.Contracts; 4 | using System.Linq; 5 | using System.Runtime.CompilerServices; 6 | using System.Threading.Tasks; 7 | 8 | #pragma warning disable CA1033 9 | 10 | namespace dotnetCampus.Configurations.Core 11 | { 12 | /// 13 | /// 提供一个异步 配置管理仓库的基类。 14 | /// 15 | public abstract class AsynchronousConfigurationRepo : IConfigurationRepo 16 | { 17 | /// 18 | /// 创建一个使用强类型的用于提供给应用程序业务使用的应用程序配置管理器。 19 | /// 20 | /// 用于提供给应用程序业务使用的配置管理器。 21 | public IAppConfigurator CreateAppConfigurator() => new ConcurrentAppConfigurator(this); 22 | 23 | /// 24 | /// 获取指定配置项的值,如果指定的 不存在,则返回 null。 25 | /// 此方法是线程安全的。 26 | /// 27 | /// 配置项的标识符。 28 | /// 配置项的值。 29 | string? IConfigurationRepo.GetValue(string key) 30 | { 31 | VerifyKey(key); 32 | 33 | return TryReadAsync(key).Result; 34 | } 35 | 36 | /// 37 | /// 设置指定配置项的值,如果设置为 null,可能删除 配置项。 38 | /// 此方法是线程安全的。 39 | /// 40 | /// 配置项的标识符。 41 | /// 配置项的值。 42 | void IConfigurationRepo.SetValue(string key, string? value) 43 | { 44 | VerifyKey(key, true); 45 | 46 | WriteAsync(key, value).Wait(); 47 | } 48 | 49 | /// 50 | /// 删除所有满足 规则的 Key 所表示的配置项。 51 | /// 52 | /// 53 | /// 指定如何过滤 Key。当指定为 null 时,全部清除。 54 | /// 55 | void IConfigurationRepo.ClearValues(Predicate keyFilter) 56 | { 57 | keyFilter = keyFilter ?? (_ => true); 58 | var keys = GetKeys(); 59 | if (keys == null) 60 | { 61 | throw new InvalidOperationException($"重写 {nameof(GetKeys)} 方法时,不应该返回 null,而应该返回空集合。"); 62 | } 63 | 64 | RemoveKeys().Wait(); 65 | 66 | async Task RemoveKeys() 67 | { 68 | var isChanged = false; 69 | foreach (var key in keys.Where(keyFilter.Invoke)) 70 | { 71 | isChanged = true; 72 | await RemoveValueCoreAsync(key).ConfigureAwait(false); 73 | } 74 | 75 | if (isChanged) 76 | { 77 | // 通知子类处理值的改变,并收集改变后果。 78 | // 此部分内容不属于 ClearValues,所以不等待。 79 | #pragma warning disable 4014 80 | NotifyChanged(keys); 81 | #pragma warning restore 4014 82 | } 83 | } 84 | } 85 | 86 | /// 87 | /// 尝试读取配置文件信息 88 | /// 89 | /// 90 | /// 如果无法读取到信息,默认返回的值 91 | /// 92 | public async Task TryReadAsync(string key, string @default = "") 93 | { 94 | if (@default == null) 95 | { 96 | throw new ArgumentNullException(nameof(@default)); 97 | } 98 | 99 | VerifyKey(key); 100 | 101 | var value = await ReadValueCoreAsync(key).ConfigureAwait(false); 102 | return value ?? @default; 103 | } 104 | 105 | /// 106 | /// 写入配置文件信息 107 | /// 108 | /// 109 | /// 110 | public async Task WriteAsync(string key, string? value) 111 | { 112 | VerifyKey(key, true); 113 | 114 | // 如果值没有变化,则不做任何处理。 115 | var originalValue = await TryReadAsync(key).ConfigureAwait(false); 116 | if (originalValue.Equals(value, StringComparison.InvariantCulture)) 117 | { 118 | return; 119 | } 120 | 121 | // 由子类处理值的改变。 122 | if (value is null || string.IsNullOrEmpty(value)) 123 | { 124 | await RemoveValueCoreAsync(key).ConfigureAwait(false); 125 | } 126 | else 127 | { 128 | await WriteValueCoreAsync(key, value).ConfigureAwait(false); 129 | } 130 | 131 | // 通知子类处理值的改变,并收集改变后果。 132 | // 此部分内容不属于 WriteAsync,所以不等待。 133 | #pragma warning disable 4014 134 | NotifyChanged(new[] { key }); 135 | #pragma warning restore 4014 136 | } 137 | 138 | private async Task NotifyChanged(IEnumerable keys) 139 | { 140 | var context = new AsynchronousConfigurationChangeContext(keys); 141 | OnChanged(context); 142 | 143 | var asyncAction = context.GetTrackedAction(); 144 | if (asyncAction != null) 145 | { 146 | await asyncAction.ConfigureAwait(false); 147 | } 148 | } 149 | 150 | /// 151 | /// 派生类重写此方法时,返回所有目前已经存储的 Key 的集合。 152 | /// 153 | protected abstract ICollection GetKeys(); 154 | 155 | /// 156 | /// 派生类重写此方法时,返回指定 Key 的值,如果不存在,需要返回 null。 157 | /// 158 | /// 指定项的 Key。 159 | /// 160 | /// 执行项的 Key,如果不存在,则为 null / Task<string>.FromResult(null)"/>。 161 | /// 162 | protected abstract Task ReadValueCoreAsync(string key); 163 | 164 | /// 165 | /// 派生类重写此方法时,将为指定的 Key 存储指定的值。 166 | /// 167 | /// 指定项的 Key。 168 | /// 要存储的值。 169 | protected abstract Task WriteValueCoreAsync(string key, string value); 170 | 171 | /// 172 | /// 派生类重写此方法时,将为指定的 Key 清除。 173 | /// 174 | /// 指定项的 Key。 175 | protected abstract Task RemoveValueCoreAsync(string key); 176 | 177 | /// 178 | /// 派生类重写此方法时,可以考虑将配置进行持久化。 179 | /// 180 | protected abstract void OnChanged(AsynchronousConfigurationChangeContext context); 181 | 182 | /// 183 | /// 为 事件提供异步追踪参数。 184 | /// 185 | protected sealed class AsynchronousConfigurationChangeContext 186 | { 187 | internal AsynchronousConfigurationChangeContext(IEnumerable changedKeys) 188 | { 189 | changedKeys = changedKeys ?? throw new ArgumentNullException(nameof(changedKeys)); 190 | if (changedKeys is IReadOnlyCollection rc) 191 | { 192 | ChangedKeys = rc; 193 | } 194 | else 195 | { 196 | ChangedKeys = changedKeys.ToArray(); 197 | } 198 | } 199 | 200 | /// 201 | /// 获取引起此上下文改变事件的配置项的 Key。 202 | /// 203 | public IReadOnlyCollection ChangedKeys { get; } 204 | 205 | /// 206 | /// 要求配置的保存过程跟踪此异步操作,使得后续的状态处理必须在此异步操作结束之后才能执行。 207 | /// 208 | /// 异步操作。 209 | public void TrackAsyncAction(Task action) => 210 | _trackedAction = action ?? throw new ArgumentNullException(nameof(action)); 211 | 212 | /// 213 | /// 获取此对象储存的跟踪的异步操作。 214 | /// 215 | public Task? GetTrackedAction() => _trackedAction; 216 | 217 | private Task? _trackedAction; 218 | } 219 | 220 | /// 221 | /// 验证字符串 能否成为配置项的键。 222 | /// 223 | /// 配置项的键。 224 | /// 225 | /// 如果调用此验证方法时,你需要使用这个键创建一个新的配置项(而不是读取原有配置),那么需要在这里传入 true。 226 | /// 当设为 true 时,此方法会额外验证键是否是单行的(不带换行符)。 227 | /// 228 | [ContractArgumentValidator, MethodImpl(MethodImplOptions.AggressiveInlining)] 229 | // ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local 230 | private static void VerifyKey(string key, bool createNew = false) 231 | { 232 | if (key == null) 233 | { 234 | throw new ArgumentNullException(nameof(key)); 235 | } 236 | 237 | if (string.IsNullOrEmpty(key)) 238 | { 239 | throw new ArgumentException("不允许使用空字符串作为配置项的 Key。", nameof(key)); 240 | } 241 | 242 | if (createNew && (key.Contains('\n') || key.Contains('\r'))) 243 | { 244 | throw new ArgumentException("不允许使用换行符串作为配置项的 Key。", nameof(key)); 245 | } 246 | 247 | Contract.EndContractBlock(); 248 | } 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /src/dotnetCampus.Configurations/Configuration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.Globalization; 4 | using System.Runtime.CompilerServices; 5 | using dotnetCampus.Configurations.Converters; 6 | using dotnetCampus.Configurations.Core; 7 | 8 | #pragma warning disable CA1724 9 | 10 | namespace dotnetCampus.Configurations 11 | { 12 | /// 13 | /// 包含一组配置项。 14 | /// 为所有的应用程序配置项提供与 的交互。 15 | /// 派生类继承此基类时,添加属性以存储配置。 16 | /// 17 | public abstract class Configuration 18 | { 19 | /// 20 | /// 创建 类型的新实例。 21 | /// 22 | protected Configuration() 23 | { 24 | var name = this.GetClassNameWithoutSuffix(); 25 | _section = name + "."; 26 | } 27 | 28 | /// 29 | /// 创建 类型的新实例,当存储值时,其前准为 。 30 | /// 31 | protected Configuration(string? sectionName) 32 | => _section = string.IsNullOrEmpty(sectionName) ? "" : $"{sectionName}."; 33 | 34 | /// 35 | /// 在派生类中为属性的 get 访问器提供获取配置值的方法。 36 | /// 此方法会返回可空值类型,如果配置项不存在或不是布尔类型,则值为 null。 37 | /// 38 | /// 配置项的标识符,自动从属性名中获取。 39 | /// 配置项的值。 40 | protected bool? GetBoolean([CallerMemberName] string? key = null) 41 | { 42 | var value = GetValue(key); 43 | return value.AsBoolean(); 44 | } 45 | 46 | /// 47 | /// 在派生类中为属性的 get 访问器提供获取配置值的方法。 48 | /// 此方法会返回可空值类型,如果配置项不存在或不是数值类型,则值为 null。 49 | /// 50 | /// 配置项的标识符,自动从属性名中获取。 51 | /// 配置项的值。 52 | protected decimal? GetDecimal([CallerMemberName] string? key = null) 53 | { 54 | var value = GetValue(key); 55 | return value.AsDecimal(); 56 | } 57 | 58 | /// 59 | /// 在派生类中为属性的 get 访问器提供获取配置值的方法。 60 | /// 此方法会返回可空值类型,如果配置项不存在或不是数值类型,则值为 null。 61 | /// 62 | /// 配置项的标识符,自动从属性名中获取。 63 | /// 配置项的值。 64 | protected double? GetDouble([CallerMemberName] string? key = null) 65 | { 66 | var value = GetValue(key); 67 | return value.AsDouble(); 68 | } 69 | 70 | /// 71 | /// 在派生类中为属性的 get 访问器提供获取配置值的方法。 72 | /// 此方法会返回可空值类型,如果配置项不存在或不是数值类型,则值为 null。 73 | /// 74 | /// 配置项的标识符,自动从属性名中获取。 75 | /// 配置项的值。 76 | protected float? GetSingle([CallerMemberName] string? key = null) 77 | { 78 | var value = GetValue(key); 79 | return value.AsSingle(); 80 | } 81 | 82 | /// 83 | /// 在派生类中为属性的 get 访问器提供获取配置值的方法。 84 | /// 此方法会返回可空值类型,如果配置项不存在或不是整数类型,则值为 null。 85 | /// 86 | /// 配置项的标识符,自动从属性名中获取。 87 | /// 配置项的值。 88 | protected int? GetInt32([CallerMemberName] string? key = null) 89 | { 90 | var value = GetValue(key); 91 | return value.AsInt32(); 92 | } 93 | 94 | /// 95 | /// 在派生类中为属性的 get 访问器提供获取配置值的方法。 96 | /// 此方法会返回可空值类型,如果配置项不存在或不是整数类型,则值为 null。 97 | /// 98 | /// 配置项的标识符,自动从属性名中获取。 99 | /// 配置项的值。 100 | protected long? GetInt64([CallerMemberName] string? key = null) 101 | { 102 | var value = GetValue(key); 103 | return value.AsInt64(); 104 | } 105 | 106 | /// 107 | /// 在派生类中为属性的 get 访问器提供获取配置值的方法。 108 | /// 此方法会返回可空值类型,如果配置项不存在或不是指定的枚举类型,则值为 null。 109 | /// 110 | /// 配置项的标识符,自动从属性名中获取。 111 | /// 配置项的值。 112 | protected T? GetEnum([CallerMemberName] string? key = null) 113 | where T : struct, IConvertible 114 | { 115 | var value = GetValue(key); 116 | return value.AsEnum(); 117 | } 118 | 119 | /// 120 | /// 在派生类中为属性的 get 访问器提供获取配置值的方法。 121 | /// 如果配置中没有储存值: 122 | /// 1. 如果你当作字符串使用,则会获取到 ,即不可能为 null; 123 | /// 2. 你也可以使用 ?? 操作符以便指定默认值,写法形如 `get => GetString() ?? "DefaultValue"`。 124 | /// 假设你只打算使用字符串,请调用 `ToString()` 方法,这将返回一个永不为 null 的字符串( 代表默认值)。 125 | /// 126 | /// 配置项的标识符,自动从属性名中获取。 127 | /// 配置项的值。 128 | protected ConfigurationString? GetString([CallerMemberName] string? key = null) 129 | => GetValue(key); 130 | 131 | /// 132 | /// 在派生类中为属性的 get 访问器提供获取配置值的方法。 133 | /// 如果指定配置项的值不存在,则返回空字符串。 134 | /// 135 | /// 配置项的标识符,自动从属性名中获取。 136 | /// 配置项的值。 137 | internal ConfigurationString? GetValue([CallerMemberName] string? key = null) 138 | { 139 | if (key is null) 140 | { 141 | throw new ArgumentNullException(nameof(key)); 142 | } 143 | 144 | if (Repo == null) 145 | { 146 | throw new InvalidOperationException($"必须通过 {nameof(IAppConfigurator)}.Of 使用配置。"); 147 | } 148 | 149 | var value = Repo.GetValue($"{_section}{key}"); 150 | return (ConfigurationString?) value; 151 | } 152 | 153 | /// 154 | /// 在派生类中为属性的 set 访问器提供设置配置值的方法。 155 | /// 156 | /// 配置项的值。 157 | /// 配置项的标识符,自动从属性名中获取。 158 | protected void SetValue(bool? value, [CallerMemberName] string? key = null) 159 | => SetValue(value?.ToString(CultureInfo.InvariantCulture) ?? "", key); 160 | 161 | /// 162 | /// 在派生类中为属性的 set 访问器提供设置配置值的方法。 163 | /// 164 | /// 配置项的值。 165 | /// 配置项的标识符,自动从属性名中获取。 166 | protected void SetValue(decimal? value, [CallerMemberName] string? key = null) 167 | => SetValue(value?.ToString(CultureInfo.InvariantCulture) ?? "", key); 168 | 169 | /// 170 | /// 在派生类中为属性的 set 访问器提供设置配置值的方法。 171 | /// 172 | /// 配置项的值。 173 | /// 配置项的标识符,自动从属性名中获取。 174 | protected void SetValue(double? value, [CallerMemberName] string? key = null) 175 | => SetValue(value?.ToString(CultureInfo.InvariantCulture) ?? "", key); 176 | 177 | /// 178 | /// 在派生类中为属性的 set 访问器提供设置配置值的方法。 179 | /// 180 | /// 配置项的值。 181 | /// 配置项的标识符,自动从属性名中获取。 182 | protected void SetValue(float? value, [CallerMemberName] string? key = null) 183 | => SetValue(value?.ToString(CultureInfo.InvariantCulture) ?? "", key); 184 | 185 | /// 186 | /// 在派生类中为属性的 set 访问器提供设置配置值的方法。 187 | /// 188 | /// 配置项的值。 189 | /// 配置项的标识符,自动从属性名中获取。 190 | protected void SetValue(int? value, [CallerMemberName] string? key = null) 191 | => SetValue(value?.ToString(CultureInfo.InvariantCulture) ?? "", key); 192 | 193 | /// 194 | /// 在派生类中为属性的 set 访问器提供设置配置值的方法。 195 | /// 196 | /// 配置项的值。 197 | /// 配置项的标识符,自动从属性名中获取。 198 | protected void SetValue(long? value, [CallerMemberName] string? key = null) 199 | => SetValue(value?.ToString(CultureInfo.InvariantCulture) ?? "", key); 200 | 201 | /// 202 | /// 在派生类中为属性的 set 访问器提供设置配置值的方法。 203 | /// 204 | /// 配置项的值。 205 | /// 配置项的标识符,自动从属性名中获取。 206 | protected void SetEnum(T? value, [CallerMemberName] string? key = null) 207 | where T : struct, IConvertible 208 | => SetValue(value?.ToString() ?? "", key); 209 | 210 | /// 211 | /// 在派生类中为属性的 set 访问器提供设置配置值的方法。 212 | /// 不允许 为 null;如果需要存入空值,请使用 。 213 | /// 214 | /// 配置项的值。 215 | /// 配置项的标识符,自动从属性名中获取。 216 | protected internal void SetValue(ConfigurationString? value, [CallerMemberName] string? key = null) 217 | { 218 | if (key is null) 219 | { 220 | throw new ArgumentNullException(nameof(key)); 221 | } 222 | 223 | if (Repo == null) 224 | { 225 | throw new InvalidOperationException($"必须通过 {nameof(IAppConfigurator)}.Of 使用配置。"); 226 | } 227 | 228 | // value.ToString() 可以拿到一定非 null 的字符串; 229 | // value?.ToString() 则可以在字符串为 null/"" 时拿到 null。 230 | Repo.SetValue($"{_section}{key}", value?.ToString()); 231 | } 232 | 233 | /// 234 | /// 清除此类型的所有配置项。此方法调用后,此类型中的所有属性将被设为默认值。 235 | /// 236 | protected void ClearValues() 237 | { 238 | if (Repo == null) 239 | { 240 | throw new InvalidOperationException($"必须通过 {nameof(IAppConfigurator)}.Of 使用配置。"); 241 | } 242 | 243 | Repo.ClearValues(key => key.StartsWith(_section, StringComparison.InvariantCulture)); 244 | } 245 | 246 | /// 247 | /// 获取配置属性标识符的类型前缀。 248 | /// 249 | private readonly string _section; 250 | 251 | /// 252 | /// 获取用于管理应用程序字符串配置项的管理器。 253 | /// 254 | internal IConfigurationRepo? Repo { get; set; } 255 | 256 | internal IAppConfigurator? AppConfigurator { get; set; } 257 | 258 | /// 259 | /// 尝试获取 实例。只有配置框架内部创建的配置,才能获取到 实例。 260 | /// 261 | /// 262 | /// 263 | public bool TryGetAppConfigurator 264 | ( 265 | #if NETCOREAPP3_0_OR_GREATER 266 | [NotNullWhen(true)] 267 | #endif 268 | out IAppConfigurator? appConfigurator 269 | ) 270 | { 271 | appConfigurator = AppConfigurator; 272 | return appConfigurator != null; 273 | } 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /src/dotnetCampus.Configurations/Utils/WeakEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using System.Runtime.CompilerServices; 6 | 7 | namespace dotnetCampus.WeakEvents 8 | { 9 | /// 10 | /// 定义一个弱事件。 11 | /// 此类型的所有方法是线程安全的。 12 | /// 13 | /// 事件源类型。如果是常见的 类型,可以使用 泛型类型。 14 | /// 事件参数类型。如果不知道事件参数的类型,可以查看委托定义中事件参数的定义。 15 | /// 16 | /// 有两种用法: 17 | /// 1. 在事件源定义事件的时候使用,这可以使得此事件不会强引用事件的订阅者; 18 | /// 2. 配合 做一个弱事件中继,为库中原来没有做弱事件的类型添加弱事件支持。 19 | /// 有关此类型的两种不同用法,请参阅文档: 20 | /// 1. https://blog.walterlv.com/post/implement-custom-dotnet-weak-event.html 21 | /// 2. https://blog.walterlv.com/post/implement-custom-dotnet-weak-event-relay.html 22 | /// 23 | internal class WeakEvent 24 | { 25 | /// 26 | /// 提供线程安全的锁。 27 | /// 28 | private readonly object _locker = new object(); 29 | 30 | /// 31 | /// 保留所有已订阅了弱事件的对象的弱引用实例。 32 | /// 33 | /// 例如,某对象用了 a.Changed += this.a_Changed; 来注册事件,那么这里将保存 this 的实例。 34 | /// 35 | /// 36 | /// 这是因为,对于以上订阅,a_Changed 是一个方法,这里编译器会利用隐式转换将方法转换为事件对应的 EventHandler 的新实例。 37 | /// 由于每次转换都会生成不同的实例,所以如果保留此实例后依然弱引用,将会被 GC 回收。 38 | /// 因此我们需要保留原始的对象实例,来保证事件注册的生命周期与原始对象的生命周期相同。 39 | /// 40 | /// 41 | private readonly List> _relatedInstances = new List>(); 42 | 43 | /// 44 | /// 包含所有事件订阅的原始对象到当前已订阅的事件处理函数的弱引用关系。 45 | /// 46 | /// 47 | /// 注意,这里使用的是 类型。 48 | /// 它可以存储一组键值对,但键和值均不是强引用的。但注意,它不是字典,不可枚举不可遍历! 49 | /// 如果键没有被回收,那么值一定不会被回收;如果键被回收,那么值在没有被引用的情况也会被回收。 50 | /// 另外,如果你不能传入 Key 的实例,你绝无可能找到 Value! 51 | /// 另请参见:https://blog.walterlv.com/post/conditional-weak-table.html 52 | /// 53 | private readonly ConditionalWeakTable _handlers = new ConditionalWeakTable(); 54 | 55 | /// 56 | /// 订阅弱事件处理函数。 57 | /// 58 | /// 原始处理函数,请始终传入 value。 59 | /// 可被隐式转换为 Action 的方法组,请始终传入 value.Invoke。 60 | public void Add(MulticastDelegate originalHandler, Action castedHandler) 61 | { 62 | lock (_locker) 63 | { 64 | // 获取委托对应的目标实例。 65 | var target = originalHandler.Target ?? throw new ArgumentException("无法将弱事件应用于没有对象的纯函数中。", nameof(originalHandler)); 66 | var method = originalHandler.Method; 67 | 68 | // 找到目前是否有已经存储过的对 target 的弱引用实例,如果有,我们将复用此实例,而不是加入到集合中。 69 | // 注意,这里的判定使用的是 ReferenceEquals,因为 ConditionalWeakTable 的比较用的是此方法,这可以确保回收时机两者一致。 70 | var reference = _relatedInstances.Find(x => x.TryGetTarget(out var instance) && ReferenceEquals(target, instance)); 71 | if (reference is null) 72 | { 73 | // 如果没有找到已经存储过的弱引用实例,我们将创建一个新的。 74 | reference = new WeakReference(target); 75 | _relatedInstances.Add(reference); 76 | var weakEventHandler = new WeakEventHandler(); 77 | weakEventHandler.Add(originalHandler, castedHandler); 78 | _handlers.Add(target, weakEventHandler); 79 | } 80 | else if (_handlers.TryGetValue(target, out var weakEventHandler)) 81 | { 82 | // 如果找到了已经存储过的弱引用实例,则为其添加一个新的事件处理器。 83 | weakEventHandler.Add(originalHandler, castedHandler); 84 | } 85 | else 86 | { 87 | // 如果找不到弱引用实例,说明有一个已经被 GC 掉的对象竟然还能 += 事件。逗我?! 88 | throw new InvalidOperationException("有一个已经被 GC 掉的对象正在试图注册事件处理函数,可能代码写错了。"); 89 | } 90 | } 91 | } 92 | 93 | /// 94 | /// 注销弱事件处理函数。 95 | /// 96 | /// 原始处理函数,请始终传入 value。 97 | public void Remove(MulticastDelegate originalHandler) 98 | { 99 | lock (_locker) 100 | { 101 | // 获取委托对应的目标实例。 102 | var target = originalHandler.Target ?? throw new ArgumentException("无法将弱事件应用于没有方法的纯对象中。", nameof(originalHandler)); 103 | 104 | // 找到目前是否有已经存储过的对 target 的弱引用实例,如果有,我们将复用此实例,而不是加入到集合中。 105 | // 注意,这里的判定使用的是 ReferenceEquals,因为 ConditionalWeakTable 的比较用的是此方法,这可以确保回收时机两者一致。 106 | var reference = _relatedInstances.Find(x => x.TryGetTarget(out var instance) && ReferenceEquals(target, instance)); 107 | if (reference is null) 108 | { 109 | // 如果都没有找到已经存储过的弱引用实例,那我们还移除个啥,有什么好移除的? 110 | } 111 | else if (_handlers.TryGetValue(target, out var weakEventHandler)) 112 | { 113 | // 如果找到了已经存储过的弱引用实例,则注销此事件处理器。 114 | weakEventHandler.Remove(originalHandler); 115 | } 116 | else 117 | { 118 | // 如果找不到弱引用实例,说明有一个已经被 GC 掉的对象竟然还能 -= 事件。逗我?! 119 | throw new InvalidOperationException("有一个已经被 GC 掉的对象正在试图注销事件处理函数,可能代码写错了。"); 120 | } 121 | } 122 | } 123 | 124 | /// 125 | /// 引发弱事件,并传入事件引发源和事件参数。 126 | /// 127 | /// 事件引发源。 128 | /// 事件参数。 129 | /// 130 | /// 如果在引发事件后发现已经没有任何对象订阅了此事件,则返回 false,这表明可以着手回收事件中继了。 131 | /// 相反,如果返回了 true,说明还有存活的对象正在订阅此事件。 132 | /// 133 | public bool Invoke(TSender sender, TArgs e) 134 | { 135 | List>? invokingHandlers = null; 136 | lock (_locker) 137 | { 138 | var weakEventHandlerList = _relatedInstances.ConvertAll(x => 139 | // 从原始的委托集合中查找需要引发事件的对象。 140 | x.TryGetTarget(out var relatedInstance) 141 | // 如果能找到目标对象,那么从 ConditionalWeakTable 中查找对应的弱事件处理器(实际上只要上面的委托存在,这里就 100% 一定存在,所以实际上我们只是为了拿 value)。 142 | && _handlers.TryGetValue(relatedInstance, out var value) 143 | // 如果找到了弱事件处理器,那么返回此处理器。 144 | ? value 145 | // 如果没有找到弱事件处理器,那么返回 null,等待被过滤。 146 | : null); 147 | 148 | // 确认订阅事件的原始对象是否仍然存活。 149 | var anyHandlerAlive = weakEventHandlerList.Exists(x => x != null); 150 | if (anyHandlerAlive) 151 | { 152 | // 如果依然存活,则引发事件(无论是否还剩余订阅,这可以与一般事件行为保持一致)。 153 | invokingHandlers = weakEventHandlerList.OfType().SelectMany(x => x.GetInvokingHandlers()).ToList(); 154 | } 155 | else 156 | { 157 | // 如果没有存活,则回收事件中继。 158 | invokingHandlers = null; 159 | _relatedInstances.Clear(); 160 | } 161 | } 162 | 163 | if (invokingHandlers != null) 164 | { 165 | foreach (var handler in invokingHandlers) 166 | { 167 | var strongHandler = handler; 168 | strongHandler(sender, e); 169 | } 170 | } 171 | 172 | return invokingHandlers != null; 173 | } 174 | 175 | /// 176 | /// 用于关联每一个订阅弱事件的事件处理函数。 177 | /// 178 | /// 对于一次形如 Target.Changed += Target_Changed 的事件注册,编译器会隐式将方法组 Target_Changed 转换成新的事件处理函数实例;我们需要代替的,就是这个新的事件处理函数实例。 179 | /// 180 | /// 181 | private sealed class WeakEventHandler 182 | { 183 | internal void Add(MulticastDelegate handler, Action castedHandler) 184 | { 185 | if (handler is null) 186 | { 187 | throw new ArgumentNullException(nameof(handler)); 188 | } 189 | 190 | if (Target != null && Target != handler.Target) 191 | { 192 | throw new ArgumentException("如果代码没有写错,不可能在这里传入不一致的 Target。", nameof(handler)); 193 | } 194 | 195 | Target = handler.Target; 196 | 197 | if (MethodHandlers.TryGetValue(handler.Method, out var handlers)) 198 | { 199 | handlers.Add(castedHandler); 200 | } 201 | else 202 | { 203 | handlers = new List> 204 | { 205 | castedHandler, 206 | }; 207 | MethodHandlers[handler.Method] = handlers; 208 | } 209 | } 210 | 211 | internal void Remove(MulticastDelegate handler) 212 | { 213 | if (handler is null) 214 | { 215 | throw new ArgumentNullException(nameof(handler)); 216 | } 217 | 218 | if (Target != null && Target != handler.Target) 219 | { 220 | throw new ArgumentException("如果代码没有写错,不可能在这里传入不一致的 Target。", nameof(handler)); 221 | } 222 | 223 | Target = handler.Target; 224 | 225 | if (MethodHandlers.TryGetValue(handler.Method, out var handlers)) 226 | { 227 | handlers.RemoveAt(handlers.Count - 1); 228 | if (handlers.Count == 0) 229 | { 230 | MethodHandlers.Remove(handler.Method); 231 | } 232 | } 233 | } 234 | 235 | internal IReadOnlyList> GetInvokingHandlers() 236 | { 237 | return MethodHandlers.SelectMany(x => x.Value).ToList(); 238 | } 239 | 240 | /// 241 | /// 获取此弱事件处理器关联的目标对象。 242 | /// 243 | internal object? Target { get; private set; } 244 | 245 | /// 246 | /// 获取此弱事件处理器关联的目标方法或方法组,以及所有基于此方法组转换而得的可以直接调用的委托。 247 | /// 在实际上引发事件的时候,应该使用此转换后的实例,以避免使用原始事件处理函数导致的反射、IL 生成等耗性能的执行。 248 | /// 249 | private Dictionary>> MethodHandlers { get; } = new Dictionary>>(); 250 | } 251 | } 252 | 253 | /// 254 | /// 定义一个弱事件。 255 | /// 此类型的所有方法是线程安全的。 256 | /// 257 | /// 事件参数类型。如果不知道事件参数的类型,可以查看委托定义中事件参数的定义。 258 | /// 259 | /// 有两种用法: 260 | /// 1. 在事件源定义事件的时候使用,这可以使得此事件不会强引用事件的订阅者; 261 | /// 2. 配合 做一个弱事件中继,为库中原来没有做弱事件的类型添加弱事件支持。 262 | /// 有关此类型的两种不同用法,请参阅文档: 263 | /// 1. https://blog.walterlv.com/post/implement-custom-dotnet-weak-event.html 264 | /// 2. https://blog.walterlv.com/post/implement-custom-dotnet-weak-event-relay.html 265 | /// 266 | internal class WeakEvent : WeakEvent 267 | { 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /tests/dotnetCampus.Configurations.Tests/ConfigurationTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Runtime.CompilerServices; 5 | using dotnetCampus.Configurations.Core; 6 | using dotnetCampus.Configurations.Tests.Fakes; 7 | using Microsoft.VisualStudio.TestTools.UnitTesting; 8 | using Moq; 9 | using MSTest.Extensions.Contracts; 10 | 11 | namespace dotnetCampus.Configurations.Tests 12 | { 13 | [TestClass] 14 | public class ConfigurationTests 15 | { 16 | [ContractTestCase] 17 | public void GetValueAndGetValue() 18 | { 19 | "综合测试,用于断点调试配置项执行流程。".Test(() => 20 | { 21 | // Arrange 22 | var dictionary = new Dictionary(StringComparer.Ordinal); 23 | var configuration = CreateConfiguration( 24 | key => dictionary.TryGetValue(key, out var value) ? value : string.Empty, 25 | (key, value) => dictionary[key] = value); 26 | var fake = configuration.Of(); 27 | 28 | // Act 29 | var isTested = fake.IsTested; 30 | var amount = fake.Amount; 31 | var offsetX = fake.OffsetX; 32 | var sizeX = fake.SizeX; 33 | var count = fake.Count; 34 | var count2 = fake.Count2; 35 | var length = fake.Length; 36 | var length2 = fake.Length2; 37 | var message = fake.Message; 38 | var methodImpl = fake.MethodImpl; 39 | var dateTime = fake.DateTime; 40 | var dateTimeOffset = fake.DateTimeOffset; 41 | //var bounds = fake.Bounds; 42 | //var color = fake.Color; 43 | 44 | // Assert 45 | Assert.AreEqual(null, isTested); 46 | Assert.AreEqual(null, amount); 47 | Assert.AreEqual(null, offsetX); 48 | Assert.AreEqual(null, sizeX); 49 | Assert.AreEqual(null, count); 50 | Assert.AreEqual(0, count2); 51 | Assert.AreEqual(null, length); 52 | Assert.AreEqual(0L, length2); 53 | Assert.AreEqual("", message); 54 | Assert.AreEqual(MethodImplOptions.AggressiveInlining, methodImpl); 55 | Assert.AreEqual(new DateTime(), dateTime); 56 | Assert.AreEqual(null, dateTimeOffset); 57 | //Assert.AreEqual(new Rect(10, 20, 100, 200), bounds); 58 | //Assert.AreEqual(null, color); 59 | Assert.AreEqual(0, dictionary.Count); 60 | 61 | // Act 62 | fake.IsTested = true; 63 | fake.Amount = 123.51243634523452345123514251m; 64 | fake.OffsetX = 100; 65 | fake.SizeX = 69.5f; 66 | fake.Count = 50; 67 | fake.Count2 = 50; 68 | fake.Length = 1230004132413241; 69 | fake.Length2 = 1230004132413241; 70 | fake.Message = "ABC"; 71 | fake.MethodImpl = MethodImplOptions.NoInlining; 72 | //fake.DateTime = new DateTime(2020, 12, 04, 16, 03, 49, 591, DateTimeKind.Local); 73 | fake.DateTimeOffset = new DateTimeOffset(2020, 12, 04, 16, 03, 49, 591, TimeSpan.FromHours(8)); 74 | //fake.Bounds = new Rect(10, 20, 200, 100); 75 | //fake.Color = Colors.Teal; 76 | 77 | // Assert 78 | Assert.AreEqual(true, fake.IsTested); 79 | Assert.AreEqual(123.51243634523452345123514251m, fake.Amount); 80 | Assert.AreEqual(100, fake.OffsetX); 81 | Assert.AreEqual(69.5f, fake.SizeX); 82 | Assert.AreEqual(50, fake.Count); 83 | Assert.AreEqual(50, fake.Count2); 84 | Assert.AreEqual(1230004132413241, fake.Length); 85 | Assert.AreEqual(1230004132413241, fake.Length2); 86 | Assert.AreEqual("ABC", fake.Message); 87 | //Assert.AreEqual(new DateTime(2020, 12, 04, 16, 03, 49, 591, DateTimeKind.Local), fake.DateTime); 88 | Assert.AreEqual(new DateTimeOffset(2020, 12, 04, 16, 03, 49, 591, TimeSpan.FromHours(8)), fake.DateTimeOffset); 89 | //Assert.AreEqual(new Rect(10, 20, 200, 100), fake.Bounds); 90 | Assert.AreEqual("True", dictionary["Debug.IsTested"]); 91 | Assert.AreEqual("123.51243634523452345123514251", dictionary["Debug.Amount"]); 92 | Assert.AreEqual("100", dictionary["Debug.OffsetX"]); 93 | Assert.AreEqual("69.5", dictionary["Debug.SizeX"]); 94 | Assert.AreEqual("50", dictionary["Debug.Count"]); 95 | Assert.AreEqual("50", dictionary["Debug.Count2"]); 96 | Assert.AreEqual("1230004132413241", dictionary["Debug.Length"]); 97 | Assert.AreEqual("1230004132413241", dictionary["Debug.Length2"]); 98 | Assert.AreEqual("ABC", dictionary["Debug.Message"]); 99 | Assert.AreEqual("NoInlining", dictionary["Debug.MethodImpl"]); 100 | //Assert.AreEqual("2020-12-04T16:03:49.5910000Z", dictionary["Debug.DateTime"]); 101 | Assert.AreEqual("2020-12-04T16:03:49.5910000+08:00", dictionary["Debug.DateTimeOffset"]); 102 | //Assert.AreEqual("10,20,200,100", dictionary["Debug.Bounds"]); 103 | //Assert.AreEqual("#FF008080", dictionary["Debug.Color"]); 104 | 105 | // Act 106 | //fake.Bounds = new Rect(10, 20, 100, 200); 107 | //Assert.AreEqual(null, dictionary["Debug.Bounds"]); 108 | }); 109 | 110 | "不允许使用 new 创建 Configuration,如果使用,则后续使用抛出异常。".Test(() => 111 | { 112 | // Arrange 113 | var configuration = new DebugConfiguration(); 114 | 115 | // Act & Assert 116 | // ReSharper disable once ExplicitCallerInfoArgument 117 | Assert.ThrowsException(() => configuration.GetValue("Foo")); 118 | // ReSharper disable once ExplicitCallerInfoArgument 119 | Assert.ThrowsException(() => configuration.SetValue("Bar", "Foo")); 120 | }); 121 | 122 | "默认的配置不带前缀。".Test(() => 123 | { 124 | // Arrange 125 | var dictionary = new Dictionary(); 126 | var configuration = CreateConfiguration( 127 | key => dictionary.TryGetValue(key, out var value) ? value : string.Empty, 128 | (key, value) => dictionary[key] = value); 129 | var @default = configuration.Default; 130 | 131 | // Act 132 | string defaultFoo = @default["Foo"]; 133 | @default["Foo"] = "Bar"; 134 | 135 | // Assert 136 | Assert.AreEqual("", defaultFoo); 137 | Assert.AreEqual("Bar", dictionary["Foo"]); 138 | }); 139 | 140 | "同一份配置组只会有一个实例。".Test(() => 141 | { 142 | // Arrange 143 | // ReSharper disable once RedundantArgumentDefaultValue 144 | // ReSharper disable once RedundantArgumentDefaultValue 145 | var configuration = CreateConfiguration(null, null); 146 | 147 | // Act 148 | var @default0 = configuration.Of(); 149 | var @default1 = configuration.Of(); 150 | 151 | // Assert 152 | Assert.AreSame(default0, default1); 153 | }); 154 | 155 | "配置允许引用类型存入 null 值。".Test(() => 156 | { 157 | // Arrange 158 | var configuration = CreateConfiguration(); 159 | var @default = configuration.Default; 160 | 161 | // Act & Assert 162 | @default["Foo"] = null; 163 | }); 164 | 165 | "配置允许值类型存入 null 值,取出时也为 null 值。".Test(() => 166 | { 167 | // Arrange 168 | var dictionary = new Dictionary(); 169 | var configuration = CreateConfiguration( 170 | key => dictionary.TryGetValue(key, out var value) ? value : string.Empty, 171 | (key, value) => dictionary[key] = value); 172 | var fake = configuration.Of(); 173 | 174 | // Act 175 | fake.OffsetX = null; 176 | 177 | // Assert 178 | Assert.AreEqual(null, fake.OffsetX); 179 | }); 180 | } 181 | 182 | [ContractTestCase] 183 | public void GetStringWithDefaultValue() 184 | { 185 | "如果获取 String 时,获取到了空字符串,那么可以通过 ?? 转换成默认值。".Test(() => 186 | { 187 | // Arrange 188 | var configuration = CreateConfiguration(key => string.Empty); 189 | var fake = configuration.Of(); 190 | 191 | // Act 192 | var host = fake.Host; 193 | 194 | // Assert 195 | Assert.AreEqual("https://localhost:17134", host); 196 | }); 197 | } 198 | 199 | [ContractTestCase] 200 | public void ClearValues() 201 | { 202 | "如果清除了配置项,那么之前存的所有键值就都恢复默认值。".Test(() => 203 | { 204 | // Arrange 205 | var dictionary = new Dictionary(); 206 | var configuration = CreateConfiguration( 207 | key => dictionary.TryGetValue(key, out var value) ? value : string.Empty, 208 | (key, value) => dictionary[key] = value, 209 | keyFilter => RemoveKeys(dictionary, keyFilter)); 210 | dictionary["遗留项"] = "随便"; 211 | 212 | // Act 213 | var fake = configuration.Of(); 214 | fake.Length = 100; 215 | fake.Count = 2; 216 | 217 | // Assert 218 | Assert.AreEqual(3, dictionary.Count); 219 | 220 | // Act 221 | fake.Clear(); 222 | 223 | // Assert 224 | Assert.AreEqual(null, fake.Length); 225 | Assert.AreEqual(null, fake.Count); 226 | }); 227 | } 228 | 229 | /// 230 | /// 初始化一个 的模拟实例。 231 | /// 232 | /// 模拟 方法。 233 | /// 模拟 方法。 234 | /// 模拟 方法。 235 | /// 的模拟实例。 236 | private IAppConfigurator CreateConfiguration( 237 | Func getValue = null, Action setValue = null, 238 | Action> clearValues = null) 239 | { 240 | var managerMock = new Mock(); 241 | managerMock.Setup(m => m.CreateAppConfigurator()) 242 | .Returns(new ConcurrentAppConfigurator(managerMock.Object)); 243 | managerMock.Setup(m => m.GetValue(It.IsAny())) 244 | .Returns(key => getValue?.Invoke(key)); 245 | managerMock.Setup(m => m.SetValue(It.IsAny(), It.IsAny())) 246 | .Callback((key, value) => setValue?.Invoke(key, value)); 247 | managerMock.Setup(m => m.ClearValues(It.IsAny>())) 248 | .Callback>(keyFilter => clearValues?.Invoke(keyFilter)); 249 | return managerMock.Object.CreateAppConfigurator(); 250 | } 251 | 252 | private static void RemoveKeys(Dictionary dictionary, Predicate keyFilter) 253 | { 254 | if (keyFilter == null) 255 | { 256 | dictionary.Clear(); 257 | return; 258 | } 259 | 260 | foreach (var key in dictionary.Keys.Where(keyFilter.Invoke).ToList()) 261 | { 262 | dictionary.Remove(key); 263 | } 264 | } 265 | } 266 | } 267 | --------------------------------------------------------------------------------